P10031 「Cfz Round 3」Xor with Gcd
裴蜀定理,异或/打表

打表可以发现 n n n奇数,这一坨就是 n n n,否则是 n ⊕ n / 2 n\oplus n/2 n⊕n/2
也可以推出来,首先这种循环累加问题,肯定只有少部分产生贡献,一个经典模型是,对称位置可以抵消或者是特殊值,比如经典的 1 + 2 + . . + n 1+2+..+n 1+2+..+n。并且这是异或,考虑是不是可能存在抵消。
注意到这是 g c d gcd gcd,根据裴蜀定理有 g c d ( n , i ) = g c d ( n , n − i ) gcd(n,i)=gcd(n,n-i) gcd(n,i)=gcd(n,n−i)。这两项正好在这个异或序列的对称位置,抵消了,只会剩下中间的无法抵消,因此答案如上。
裴蜀定理不能硬背,重在理解。首先可以从辗转相除,辗转相减来理解,注意到辗转相减其实就是把 g c d ( x , y ) = g c d ( x , y − x ) gcd(x,y)=gcd(x,y-x) gcd(x,y)=gcd(x,y−x),辗转相除则是加速这个减的过程,把一段完全相同的相减操作一次完成了;从更本质的角度来理解, x = k 1 g , y = k 2 g x=k_1g,y=k_2g x=k1g,y=k2g,那 y − x = ( k 2 − k 1 ) g y-x=(k_2-k_1)g y−x=(k2−k1)g,所以 g c d ( x , y − x ) = g c d ( k 1 g , ( k 2 − k 1 ) g ) = g gcd(x,y-x)=gcd(k_1g,(k_2-k_1)g)=g gcd(x,y−x)=gcd(k1g,(k2−k1)g)=g
B. Add 0 or K
取模

相加固定值后不互质,也就是都变成某一个数字的倍数,一个显然的东西是可以都变成偶数,但 k k k是偶数时加 k k k不会改变奇偶性,这不对。考虑都变成 k k k的倍数呢?加 k k k无法改变 m o d k \mod k modk的结果,无意义。
但考虑 m o d ( k + 1 ) \mod (k+1) mod(k+1), + k +k +k可以让一个数在模 k + 1 k+1 k+1意义下 − 1 -1 −1,而 m o d ( k + 1 ) \mod (k+1) mod(k+1)的取值只有 [ 0 , k ] [0,k] [0,k],所以每个数,我们最多 k k k次,就可以把它的 m o d ( k + 1 ) \mod (k+1) mod(k+1)余数变成 0 0 0。
Happy Tree Party
树剖/并查集+暴力

一个利用性质的做法是,注意到除法下取整,对于不为 1 1 1的元素,最多 log \log log次就能变成 0 0 0,所以可以考虑暴力走 x , y x,y x,y路径,类似找 l c a lca lca, x , y x,y x,y里深度较大的往上跳,直到他们的父亲相同。跳的过程中把非 1 1 1边除掉, 1 1 1边应该跳过,这可以用并查集,把改为 1 1 1的边连接的点合并成一个点,也就是合并到最上面那个点,这样可以不经过 1 1 1的边。这是对的是因为保证了修改边权只会变小,也就是并查集合并了的点不会再分开。
但太麻烦,另一个简单的做法是,首先需要知道 f l o o r ( f l o o r ( x / y ) / z ) = f l o o r ( x / ( y z ) ) floor(floor(x/y)/z)=floor(x/(yz)) floor(floor(x/y)/z)=floor(x/(yz)),也就是下取整这玩意可以先把分母都乘起来再除,那就变成树上查询路径乘积,树剖即可。注意这是边权,我们需要把边权,下放到每条边上深度较大的点上。并且这样的话 L C A ( x , y ) LCA(x,y) LCA(x,y)的点权不该产生贡献,但直接查时包含了这个点的贡献,所以需要排除掉 L C A LCA LCA的贡献。
这个做法其它需要注意的点是,树根没有被下放的边权,在维护乘积的条件下,无贡献应该设成 1 1 1。并且边权很大,全是 1 e 18 1e18 1e18,怎么搞都会爆,考虑把乘起来大于 l o n g l o n g long long longlong的结果表示成 2 e 18 2e18 2e18,这是对的是因为 v a l val val最大也就是 1 e 18 1e18 1e18,更大的边权乘积,拿来除都是 0 0 0了。
B. Optimal Partition
线段树优化 划分型dp

划分子数组,每个划分有个分数,求最优划分。朴素转移就是 f i = m a x ( f j + c a l ( j , i ) ) f_i=max(f_j+cal(j,i)) fi=max(fj+cal(j,i)),这是 O ( n 2 ) O(n^2) O(n2)的,一般都结合数据机构优化转移。
c a l ( j , i ) cal(j,i) cal(j,i)和子数组 [ j + 1 , i ] [j+1,i] [j+1,i]长度相关,一般可以把长度表示成 i − j i-j i−j,然后把 i , j i,j i,j相关的分别放在一起,那么转移就变成了
f i = m a x ( f j + i − j ) = m a x ( f j − j ) + i , s i − s j > 0 f_i=max(f_j+i-j)=max(f_j-j)+i,s_i-s_j>0 fi=max(fj+i−j)=max(fj−j)+i,si−sj>0
f i = m a x ( f j + j − i ) = m a x ( f j + j ) − i , s i − s j < 0 f_i=max(f_j+j-i)=max(f_j+j)-i,s_i-s_j<0 fi=max(fj+j−i)=max(fj+j)−i,si−sj<0
f i = m a x ( f j ) , s i − s j = 0 f_i=max(f_j),s_i-s_j=0 fi=max(fj),si−sj=0
三个线段树分别维护 f i + i , f i − i , f i f_i+i,f_i-i,f_i fi+i,fi−i,fi的区间最值即可。每次查询是在前缀和 s i s_i si的一个区间内查询,这需要我们的线段树把 s i s_i si作为区间下标, s i s_i si可能 1 e 9 1e9 1e9,但只有 O ( n ) O(n) O(n)个不同值,所以先把 s i s_i si都离散化,然后再作为下标。
初始化 f 0 = 0 f_0=0 f0=0,其它都是 f i = − i n f f_i=-inf fi=−inf。注意这是前缀和问题,所以长度为 0 0 0的前缀需要在第一次转移之前准备好,也是一种可能,也就是把 f 0 f_0 f0先插入线段树
E. Making Anti-Palindromes
两两消除 贪心

套了个回文串,本质上是经典问题,类似力扣双周这道题
两个数组,要求相同下标位置的值都不同,每次可以交换数组 a a a的任意两个元素,问是否有解,以及最少交换次数?
首先,设两个数组分别是 a , b a,b a,b, l e n ( a ) = n len(a)=n len(a)=n,如果有一种元素,在两个数组中出现总次数超过 n n n,不管怎么排列,根据鸽巢原理,一定有一个位置, a i , b i a_i,b_i ai,bi都是这个元素,无解
如果有解,我们把 a i = b i a_i=b_i ai=bi的位置拿出来,显然最优思路是,每次交换两个位置的 a i , a j a_i,a_j ai,aj这样一次性让两个下标都不同。但问题是,能否一直有可以两两交换的 a i , a j a_i,a_j ai,aj,答案是如果有一种元素值出现次数超过总 a i a_i ai个数的一半,超出部分就不能这样交换了,只能和另一个 a i ! = b i a_i!=b_i ai!=bi的位置交换,这样相当于一次交换之处理了一个 a i a_i ai,最红总的需要次数是 a i a_i ai里的最大的元素出现次数,如果没有一个出现超过一半的元素,每次都交换两个,消除两个 a i = b i a_i=b_i ai=bi,最后如果剩一个,也就是总数是奇数,这个也需要一次,那么次数是 ( c n t + 1 ) / 2 (cnt+1)/2 (cnt+1)/2,也就是总的个数,除二上取整。
这是经典问题,抽象出来就是,有多种元素,每种有一定个数,每次可以选取两个不同种类元素一块消除,或者消除一个元素,问全部消掉的最少次数?如果不存在一种元素个数超过总数一半,就是 ( ∑ a i + 1 ) / 2 (\sum a_i+1)/2 (∑ai+1)/2,否则是 max ( a i ) \max(a_i) max(ai)
D. Colored Balls
01背包,枚举,配对贪心结论。

问所有子序列,其实也就等价于所有子集,进行配对贪心的结果的求和。
我们前面分析的很清楚了,配对贪心根据元素最大出现次数是否超过一半,贡献就两种,所以考虑贡献法,我们要是可以算出来,考虑前 i i i种元素,最大出现次数,超过/不超过总数一半的方案数,乘上贡献值,累加就行。
最大次数是否超过总数一半,需要知道最大次数和总数,总数的方案数是好求的,枚举元素种类,每种元素看成一个物品,跑01背包即可求出考虑前 i i i种元素,总数为 x x x的方案数。但最大值呢?
光跑01背包,出来的我们并不知道最大次数。但元素遍历顺序在这里并不重要,我们可以根据出现次数对元素排序,那么跑了前 i i i种元素,此时最大次数就是当前的 a i a_i ai,我们根据 a i a_i ai就能把 d p dp dp结果划分成,最大次数是否超过总数一半的两类,分别计算贡献即可。
P9242 [蓝桥杯 2023 省 B] 接龙数列
状态机dp
状态里维护当前选中序列的结尾元素的结束数位,记录符合条件的最大长度,即可得到变成符合条件最少删几个。
E1. Prime Gaming (Easy Version)

min max博弈,可以套用min-max博弈的dp框架,根据当前第几轮可以判断是谁出手,决定转移是 m i n min min还是 m a x max max。状态里需要记录剩余元素是 1 1 1还是 2 2 2,这用 m a s k mask mask的 0 / 1 0/1 0/1来记录,状态里还要记录当前还剩几个元素/已经进行了几轮,假设还剩 i i i个元素,我们的 m a s k mask mask就只使用低 i i i位。
d p ( i , m a s k ) dp(i,mask) dp(i,mask)数组保存的值是,从还剩 i i i个元素,剩余元素的 1 / 2 1/2 1/2情况为 m a s k mask mask开始玩,最后剩下的元素是 1 / 2 1/2 1/2。由于双方都是最优策略的 m i n − m a x min-max min−max博弈,这个结果肯定是确定的。
那么我们求出来所有的 d p ( n , m a s k ) dp(n,mask) dp(n,mask),累加结果是 1 / 2 1/2 1/2,即可得到所有初始方案的结果求和。
这里一个小优化是, d p dp dp返回值可以用 b o o l bool bool, 1 1 1表示是 2 2 2, 0 0 0表示 1 1 1,那么答案初始默认结果全是 1 1 1,先加上 1 ∗ 2 i 1*2^i 1∗2i,然后对于返回值是 1 1 1的 d p ( n , m a s k ) dp(n,mask) dp(n,mask),每个再加上 1 1 1,表示这个情况结果是 2 2 2.
需要得到所有 m a s k mask mask的结果,也可以记搜,枚举全部 m a s k mask mask去搜索即可,记搜保证了每个状态只会访问一次,和递推复杂度是一样的
c
int f[25][1 << 20];
void solve() {
int n, m, k;
cin >> n >> m >> k;
vi ok(n);
rep(i, 1, k) {
int x;
cin >> x;
ok[x - 1] = 1;
}
if (m == 1) {
cout << 1 << '\n';
return;
}
rep(i, 1, n) {
rep(j, 0, 1 << n) {
f[i][j] = -1;
}
}
auto &&dfs = [&](auto &&dfs, int i, int mask)->bool{
if (i == 1) {
return mask;
}
if (f[i][mask] != -1) {
return f[i][mask];
}
bool res;
//bob
if ((n - i) % 2) {
res = 1;
rep(j, 0, i - 1) {
if (!ok[j])continue;
int nmask = (mask >> (j + 1) << j) | (mask & ((1 << j) - 1));
res &= dfs(dfs, i - 1, nmask);
}
}
//alice
else {
res = 0;
rep(j, 0, i - 1) {
if (!ok[j])continue;
int nmask = (mask >> (j + 1) << j) | (mask & ((1 << j) - 1));
res |= dfs(dfs, i - 1, nmask);
}
}
return f[i][mask] = res;
};
int ans = 1 << n;
rep(mask, 0, (1 << n) - 1) {
// cout << mask << ' ' << dfs(dfs, n, mask) << '\n';
ans += dfs(dfs, n, mask);
}
cout << ans << '\n';
}
E2. Prime Gaming (Hard Version)
期望经典转化,枚举popcount
上一题的加强版, m < = 1 e 6 m<=1e6 m<=1e6
经典转化, ∑ i ∗ f ( i = x ) = ∑ f ( i > = x ) , f ( i = x ) 表示最后剩下的数是 x 的方案数 \sum i*f(i=x)=\sum f(i>=x),f(i=x)表示最后剩下的数是x的方案数 ∑i∗f(i=x)=∑f(i>=x),f(i=x)表示最后剩下的数是x的方案数。左边是我们要求的,直接求不好做,可以转化成右边,这是求期望的经典转化,两侧都除以总方案数,求的实际上就是期望。
这玩意是对的可以画图理解,左式的含义就是越大的值,贡献次数越多,而右侧式子,每个都是包含所有更大的元素,越大的值被包含的次数也越多,所以和左侧是等价的。
那么我们可以枚举 x x x,然后把元素划分成是否小于 x x x的两类,这两类仍然看可以套用我们前一问的 d p dp dp框架,求出所有 m a s k mask mask的方案数,然后如图,对于每个 m a s k mask mask,它实际对应了多种方案,因为我们是让 0 0 0表示小于 x x x, 1 1 1表示大于等于 x x x,前者可以取 [ 1 , x − 1 ] [1,x-1] [1,x−1],后者可以取 [ x , m ] [x,m] [x,m],

所以一个 m a s k mask mask方案数是 p o w ( m − x + 1 , c n t 1 ) ∗ p o w ( x − 1 , c n t 0 ) pow(m-x+1,cnt_1)*pow(x-1,cnt_0) pow(m−x+1,cnt1)∗pow(x−1,cnt0)。
那么到这里,我们枚举 x x x,每个 x x x枚举 m a s k mask mask,根据 p o p c o u n t ( m a s k ) popcount(mask) popcount(mask)累加贡献即可,但这样复杂度 ( m 2 n ) (m2^n) (m2n)还是太大。
注意到贡献只和 p o p c o u n t ( m a s k ) popcount(mask) popcount(mask)有关,所以,我们可以把 m a s k mask mask根据 p o p c o u n t popcount popcount分组,记录每种 p o p c o u n t popcount popcount的所有 m a s k mask mask的 d p ( n , m a s k ) = t r u e dp(n,mask)=true dp(n,mask)=true的个数,那么复杂度降低到 O ( n m ) O(nm) O(nm)
H. The Third Letter
带权并查集模板
需要注意的是 d i d_i di表示 i i i到他所在的集合的根的距离,
那么路径压缩时 d d d的更新是:递归返回时,父亲的 d d d已经是相对根的了,累加上即可。
合并时 d d d的更新,我们需要把作为儿子的集合的根,把他的 d d d更新为,相对于作为父亲的集合的根的结果。
比如这里把 f y fy fy合并到 f x fx fx,那么可以先算出 y y y到 f x fx fx的距离,就是 x x x到 f x fx fx距离,加上 y y y到 x x x的距离, d [ x ] + w d[x]+w d[x]+w,然后需要加上 f y fy fy到 y y y的距离, d [ y ] d[y] d[y]保存的是 y y y到 f y fy fy距离,所以需要取负。
c
int find(int x) {
if (f[x] == x)return x;
int fa = find(f[x]);
d[x] += d[f[x]];
return f[x] = fa;
}
void merge(int x, int y, int w) {
int fx = find(x), fy = find(y);
f[fy] = fx;
d[fy] = d[x] + w - d[y];
}
P1627 [CQOI2009] 中位数
枚举右,维护左。

包含 m m m长度为奇数的子数组,就是把数组在 m m m分裂,然后在前,后数组里分别选一个后缀和前缀。长度为奇数决定了 m m m恰好是中间位置的数,也就是子树比它的,小的数字个数相等,那么可以维护前后缀里,大于 m m m个数减小于 m m m个数,类似于一个扫描线前缀和,把所有后缀的结果存起来,然后枚举前缀,去数据结构里查能配对的后缀个数。
P3903 导弹拦截III
子序列dp

d p ( i , 0 / 1 ) dp(i,0/1) dp(i,0/1)表示 i i i结尾,是波峰/波谷的序列最大长度,初始化和转移都类似 L I S LIS LIS,只是根据状态 0 / 1 0/1 0/1,转移条件在上升和下降里选择,而 L I S LIS LIS转移条件一直是上升/下降
注意类 L I S LIS LIS的初始化,每个元素以自己结束,长度为 1 1 1的序列总是合法的,需要全部初始化成 f ( i , 1 ) = 1 f(i,1)=1 f(i,1)=1,当然这里要求了开始必须是波峰,所以 f ( i , 0 ) = 0 f(i,0)=0 f(i,0)=0
c
vvi f(2, vi(n + 1));
rep(i, 1, n) {
f[1][i] = 1;
f[0][i] = 0;
}
//1波峰,0波谷
int ans = 0;
rep(i, 2, n) {
rep(j, 1, i - 1) {
if (a[i] > a[j])f[1][i] = max(f[1][i], f[0][j] + 1);
if (a[i] < a[j])f[0][i] = max(f[0][i], f[1][j] + 1);
}
ans = max({ans, f[1][i], f[0][i]});
}
cout << ans;
}