2025 省选做题记录(二)
\(\text{By DaiRuichen007}\)
Round #37 - 2024.12.12
A. [CF1997F] Chips on a Line
题目大意
给定 \(n\) 个棋子放在 \(1\sim x\) 上,每次操作可以:
- 把一个 \(i+1\) 上的棋子替换成两个分别在 \(i,i-1\) 上的棋子,或执行逆操作。
- 把 \(1/2\) 上的棋子移动到 \(2/1\) 上。
求有多少种放棋子的方案使得经过上述操作,能得到的棋子数量最小值是 \(m\)。
数据范围:\(n,m\le 1000,x\le 10\)。
思路分析
容易发现最优策略一定是把棋子都放在 \(1\) 上再推平。
一个位置 \(i\) 上的棋子对应 \(Fib_i\) 个 \(1\) 上的棋子。
假设最终在 \(1\) 上有 \(w\) 个棋子,那么得到的最小棋子数量就是 \(Fib\) 进制下 \(w\) 的 \(\mathrm{popcount}\)。
因此我们只关心最终 \(1\) 上有多少棋子。
\(f_{u,i,j}\) 表示考虑位置 \([1,u]\),填了 \(i\) 个棋子,对应最终 \(1\) 上的 \(j\) 个棋子。
统计答案的时候判断 \(\mathrm{popcount}(j)=m\) 是否成立。
时间复杂度:\(\mathcal O(n^2xFib_x)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1005,MAXV=65005,MOD=998244353;
inline void add(int &x,const int &y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
int f[MAXV][MAXN],fib[50];
int pcnt(int x) {
int s=0;
for(int k=30;k>=1;--k) if(x>=fib[k]) x-=fib[k],++s;
return s;
}
signed main() {
fib[1]=fib[2]=1;
for(int i=3;i<=30;++i) fib[i]=fib[i-1]+fib[i-2];
int n,q,m;
scanf("%d%d%d",&n,&q,&m);
int up=n*fib[q];
f[0][0]=1;
for(int i=q;i;--i) {
int z=fib[i];
for(int j=0;j+z<=up;++j) for(int k=0;k<n&&k*z<=j;++k) {
add(f[j+z][k+1],f[j][k]);
}
}
int ans=0;
for(int j=1;j<=up;++j) if(pcnt(j)==m) add(ans,f[j][n]);
printf("%d\n",ans);
return 0;
}
B. [CF2002F2] Court Blue
题目大意
给定两个变量 \(x,y\) 初始为 \(0\),每次操作选择其中一个 \(+1\),要求始终保证:\(x\in[0,n],y\in[0,m],\gcd(x,y)\le 1\),求可能的最大 \(px+qy\)。
数据范围:\(n,m\le 2\times 10^7\)。
思路分析
打表观察,可以发现当 \(x,y\) 较小的时候,几乎总是有合法的构造,因此可以假设 \(n-x<B\) 或 \(m-y<B\) 的 \((x,y)\) 总是能被生成。
然后直接 dp 即可,\(f_{i,j}\) 表示 \(x=n-i,y=m-j\) 是否能得到。
时间复杂度 \(\mathcal O(B^2\log V)\),取 \(B=120\) 能通过此题。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int L=120;
bool f[L+5][L+5];
int bgcd(int x,int y) {
if(!x||!y||x==y) return x|y;
if(~x&1) return y&1?bgcd(x>>1,y):(bgcd(x>>1,y>>1)<<1);
return y&1?(x<y?bgcd((y-x)>>1,x):bgcd((x-y)>>1,y)):bgcd(x,y>>1);
}
void solve() {
int n,m,a,b;
scanf("%d%d%d%d",&n,&m,&a,&b);
memset(f,0,sizeof(f));
int u=max(1,n-L),v=max(1,m-L);
ll ans=0;
for(int i=0;i<=n-u;++i) for(int j=0;j<=m-v;++j) {
if(!i||!j) f[i][j]=1;
else if(bgcd(i+u,j+v)<=1) f[i][j]=f[i-1][j]|f[i][j-1];
if(f[i][j]) ans=max(ans,1ll*a*(i+u)+1ll*b*(j+v));
}
printf("%lld\n",ans);
}
signed main() {
int T; scanf("%d",&T);
while(T--) solve();
return 0;
}
C. [CF1995E2] Let Me Teach You a Lesson
题目大意
给定 \(a_1\sim a_{2n}\),可以选择交换 \(a_i,a_{i+n}\),最小化 \(\max(a_{2i-1}+a_{2i})-\min(a_{2i-1}+a_{2i})\)。
数据范围:\(n\le 10^5\)。
思路分析
最小化极差难以维护,可以考虑枚举 \(\min(a_{2i-1}+a_{2i})\),目标变成最小化 \(\max(a_{2i-1},a_{2i})\)。
有一个朴素的 dp:\(f_{i,0/1}\) 表示考虑了 \(a[1,i]\),是否交换了 \(a_{i},a_{i+n}\) 的方案。
转移为形如 \((\max,\min)\) 矩阵乘法,而 \(\min(a_{2i-1}+a_{2i})\) 相当于限定有一些转移是不可以选用的。
那么我们从小到大枚举可能的 \(\min(a_{2i-1}+a_{2i})\),可以使用的转移越来越少,只有 \(\mathcal O(n)\) 次修改,用线段树维护动态 dp 的过程即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int inf=2e9+5;
struct Mat {
array <array<int,2>,2> a;
Mat() { a={inf,inf,inf,inf}; }
inline friend Mat operator *(const Mat &u,const Mat &v) {
Mat w;
for(int i:{0,1}) for(int j:{0,1}) {
w.a[i][j]=min(max(u.a[i][0],v.a[0][j]),max(u.a[i][1],v.a[1][j]));
}
return w;
}
};
const int MAXN=2e5+5;
int n,a[MAXN][2];
Mat f[MAXN];
struct zKyGt1 {
Mat tr[1<<19];
int N;
void init() {
for(N=1;N<=n;N<<=1);
for(int i=1;i<(N<<1);++i) tr[i].a={0,inf,inf,0};
for(int i=1;i<=n;++i) tr[i+N]=f[i];
for(int i=N-1;i;--i) tr[i]=tr[i<<1]*tr[i<<1|1];
}
void upd(int x) {
for(tr[x+N]=f[x],x=(x+N)>>1;x;x>>=1) tr[x]=tr[x<<1]*tr[x<<1|1];
}
} T;
void solve() {
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d",&a[i][0]);
for(int i=1;i<=n;++i) scanf("%d",&a[i][1]);
vector <array<int,4>> op;
if(n%2==0) {
for(int i=1;i<=n;++i) {
if(i%2==0) { f[i].a={0,0,0,0}; continue; }
for(int x:{0,1}) for(int y:{0,1}) {
int l=a[i][x]+a[i+1][y],r=a[i][x^1]+a[i+1][y^1];
f[i].a[x][y]=max(l,r);
op.push_back({min(l,r),i,x,y});
}
}
} else {
a[n+1][0]=a[1][1],a[n+1][1]=a[1][0];
for(int i=1;i<=n;++i) {
for(int x:{0,1}) for(int y:{0,1}) {
if(i&1) f[i].a[x][y]=a[i][x]+a[i+1][y];
else f[i].a[x][y]=a[i][x^1]+a[i+1][y^1];
op.push_back({f[i].a[x][y],i,x,y});
}
}
}
T.init();
int ans=inf;
sort(op.begin(),op.end());
for(auto it:op) {
Mat &I=T.tr[1];
int v=min(I.a[0][0],I.a[1][1]);
if(v==inf) break;
ans=min(ans,v-it[0]);
f[it[1]].a[it[2]][it[3]]=inf;
T.upd(it[1]);
}
printf("%d\n",ans);
}
signed main() {
int _; scanf("%d",&_);
while(_--) solve();
return 0;
}
D. [CF2001E2] Deterministic Heap
题目大意
给定一棵高为 \(n\) 的满二叉树,进行 \(k\) 次操作,每次把一条到根的链上点权 \(+1\),容易发现形成大根堆。偶
执行两次 pop 操作,要求每次操作访问到的非叶子节点的左右儿子权值不相等,求方案数。
数据范围:\(n\le 100,k\le 500\)。
思路分析
称点权较大的儿子为重儿子,根的重儿子为重链。
能生成题目中的结构当且仅当 \(a_i\ge a_{ls}+a_{rs}\),可以进行一次 pop 操作相当于每个点的重儿子左右子树权值不同。
那么可以 dp:\(h_{i,j}\) 表示深度为 \(i\) 的堆,根节点权值为 \(j\) 的合法方案数,由于轻儿子中无限制,还要记录 \(g_{i,j}\) 表示没有 pop 限制的方案数。
然后考虑第二次 pop,此时如果 \(u\) 在重链上,那么 \(a_u\) 变成其重儿子 \(v\) 的点权 \(a_v\),而 \(a_{v}\) 变成原先 \(v\) 的重儿子 \(a_w\)。
我们的限定条件变成 \(a_w\ne a_{x}\),其中 \(x\) 是 \(u\) 的轻儿子。
因此在 dp 状态中要记录重儿子的权值,\(f_{i,j,t}\) 表示重儿子权值为 \(t\) 的方案数。
然后要讨论 \(a_w,a_x\) 的大小关系决定递归到哪个子问题中:
- 如果 \(a_w>a_x\),那么相当于 \(u\) 的重子树支持 pop 两次,从 \(f_{i-1,a_v,*}\) 转移,而 \(u\) 的轻子树不需要 pop,从 \(g_{i-1,a_x}\) 转移。
- 否则左右子树都能 pop 一次,从 \(h_{i-1,a_v,*},h_{i-1,a_x,*}\) 转移。
转移使用前缀和优化。
时间复杂度 \(\mathcal O(nk^2)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
namespace FastMod {
typedef unsigned long long ull;
typedef __uint128_t uLL;
ull b,q,r; uLL m;
inline void init(const ull &B) { b=B,m=(uLL(1)<<64)/B; }
inline ull mod(const ull &a) {
r=a-((m*a)>>64)*b;
return r>=b?r-b:r;
}
}
#define o(x) FastMod::mod(x)
int n,k,MOD,f[505][505],g[505][505],s[505],h[505],u[505][505],t[505][505];
inline void add(int &x,const int &y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
void solve() {
memset(f,0,sizeof(f));
memset(h,0,sizeof(h));
scanf("%d%d%d",&n,&k,&MOD),FastMod::init(MOD);
for(int i=1;i<=k;++i) for(int j=0;j<i&&i+j<=k;++j) f[i+j][i]+=2;
for(int i=1;i<=k;++i) for(int j=0;j<=i;++j) add(f[i][j],f[i-1][j]),u[i][j]=f[i][j];
for(int i=0;i<=k;++i) for(int j=0;i+j<=k;++j) ++h[i+j];
for(int i=1;i<=k;++i) add(h[i],h[i-1]);
for(int d=3;d<=n;++d) {
memset(t,0,sizeof(t));
for(int i=1;i<=k;++i) for(int j=0;j<=i;++j) {
t[i][j]=u[i][j],add(t[i][j],j?t[i][j-1]:0);
}
memset(g,0,sizeof(g));
for(int i=1;i<=k;++i) {
memset(s,0,sizeof(s));
for(int j=i;j>=0;--j) s[j]=f[i][j],add(s[j],s[j+1]);
for(int j=0;j<i&&i+j<=k;++j) {
g[i+j][i]=o(g[i+j][i]+2ll*h[j]*s[j+1]+2ll*t[i][j-1]*t[j][j]);
}
}
for(int i=1;i<=k;++i) for(int j=0;j<=i;++j) add(g[i][j],g[i-1][j]);
memcpy(f,g,sizeof(f));
memset(g,0,sizeof(g));
for(int i=1;i<=k;++i) for(int j=0;j<i&&i+j<=k;++j) {
g[i+j][i]=o(g[i+j][i]+2ll*t[i][i]*h[j]);
}
for(int i=1;i<=k;++i) for(int j=0;j<=i;++j) add(g[i][j],g[i-1][j]);
memcpy(u,g,sizeof(u));
memset(s,0,sizeof(s));
for(int i=0;i<=k;++i) for(int j=0;i+j<=k;++j) s[i+j]=o(s[i+j]+1ll*h[i]*h[j]);
for(int i=1;i<=k;++i) add(s[i],s[i-1]);
memcpy(h,s,sizeof(h));
}
int ans=0;
for(int i=0;i<=k;++i) add(ans,f[k][i]);
printf("%d\n",ans);
}
signed main() {
int T; scanf("%d",&T);
while(T--) solve();
return 0;
}
E. [CF2002G] Lattice Optimizing
题目大意
给定 \(n\times n\) 网格,边带权,定义路径权值为经过的边权 \(\mathrm{mex}\),求从左上走到右下的格路的最大权值。
数据范围:\(n\le 20\)。
思路分析
很显然无法优化维护 \(\mathrm{mex}(S)\) 的过程,必须爆搜,因此需要 Meet-in-Middle 平衡复杂度。
那么思路就是选取一条副对角线,每条路径恰好过其中一个点,爆搜出从起点 / 终点到对角线上每个点的路径对应的权值集合。
求答案是否 \(\ge k\) 相当于判断交点两侧路径权值集合进行 \(\mathrm{OR}\) 卷积后,是否有元素包含二进制位 \(0\sim k-1\)。
无法用 FWT 处理 \(\mathrm{OR}\) 卷积状物,因此必须在一侧枚举子集。
那么设这条副对角线在 \(x+y=k\) 上,那么左侧枚举子集并插入哈希表,右侧每条格路直接在哈希表中查值即可判定答案是否 \(\ge k\),如果满足就更新 \(k\gets k+1\) 继续判断。
时间复杂度 \(\mathcal O(n(3^k+2^{2n-k}))\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
struct HshT {
static const int MAXN=(1<<26)+5,P=3e7+1;
int sz,hd[P],to[MAXN];
ll w[MAXN];
void ins(ll x) {
int p=x%P;
for(int i=hd[p];i;i=to[i]) if(w[i]==x) return ;
w[++sz]=x,to[sz]=hd[p],hd[p]=sz;
}
bool qry(ll x) {
int p=x%P;
for(int i=hd[p];i;i=to[i]) if(w[i]==x) return true;
return false;
}
void init() {
for(int i=1;i<=sz;++i) hd[w[i]%P]=0,to[i]=w[i]=0;
sz=0;
}
} H;
int n,k,z,a[45][45],b[45][45];
const ll B=1ll<<40;
void dfs(int x,int y,ll s) {
if(x+y==k+2) {
while(H.qry((x*B)|(((1ll<<z)-1)&(~s)))) ++z;
return ;
}
if(x>1) dfs(x-1,y,s|1ll<<a[x-1][y]);
if(y>1) dfs(x,y-1,s|1ll<<b[x][y-1]);
}
void solve() {
scanf("%d",&n),H.init(),z=1;
for(int i=1;i<n;++i) for(int j=1;j<=n;++j) scanf("%d",&a[i][j]);
for(int i=1;i<=n;++i) for(int j=1;j<n;++j) scanf("%d",&b[i][j]);
k=2*(n-1)/3;
for(int p=0;p<(1<<k);++p) {
int i=1,j=1; ll s=0;
for(int o=0;o<k;++o) {
if(p>>o&1) s|=1ll<<a[i++][j];
else s|=1ll<<b[i][j++];
}
for(ll t=s;;t=(t-1)&s) {
H.ins((i*B)|t);
if(!t) break;
}
}
dfs(n,n,0);
printf("%d\n",z-1);
}
signed main() {
int T; scanf("%d",&T);
while(T--) solve();
return 0;
}
*F. [CF2002H] Counting 101
题目大意
给定一个序列 \(a\),一次操作可以将 \(a_{i-1},a_i,a_{i+1}\) 替换成 \(\max(a_{i-1}+1,a_i,a_{i+1}+1)\)。
定义一个序列的权值为:保证 \(\max a_i\le m\) 的情况下能进行最多的操作次数。
给定 \(n,m\),对于每个 \(k\) 求有多少序列 \(a_1\sim a_n\) 权值为 \(k\)。
数据范围:\(n\le 130,m\le 30\)。
思路分析
先刻画序列的权值,如果初始没有 \(a_i=m\) 的元素,那么剩下的元素个数不超过 \(2\)。
这是显然的,我们不断操作序列最中间的元素,此时最终的元素是 \(\max a_i+[i\ne mid]\le m\)。
那么如果有 \(a_i=m\) 的元素,很显然如果对这个元素操作,一定是以 \(a_i\) 为中心进行操作。
观察相邻两个 \(m\) 之间的元素,我们发现最优解有两种情况:
- 一种是先把以 \(m\) 为中心的操作全部做完,剩下 \(<m\) 的每一段元素用上述 \(\max a_i<m\) 的元素删剩下 \(1\sim 2\) 个。
- 如果想剩下 \(0\) 个元素,那么我们要先在区间内操作几次,在不产生 \(=m\) 的元素的情况下使得区间内的元素被两端 \(a_i=m\) 的元素的操作覆盖。
因此我们只关心每个 \(a_i=m\) 的位置上操作了几次,记为 \(g_{i,j}\),表示第 \(i\) 个 \(m\) 上操作了 \(j\) 次,最少能剩下几个元素。
转移就是 \(g_{i-1,j_1}\to g_{i,j_2}\),设两个 \(m\) 中间夹着的连续段长度为 \(L\),转移系数就是这个 \(L\) 个元素最少能剩下几个:
-
首先要求 \(j_1+j_2\le L\)。
-
如果 \(L\bmod 2=1\),那么转移系数就是 \(+1\)。
-
否则考虑这一段区间内部,在值域 \([1,m-1]\) 的情况下最少剩几个元素,很显然这就是值域 \(m-1\) 的子问题,设答案为 \(p\)。
如果 \(j_1+j_2\ge p\),系数是 \(0\),否则系数是 \(1\)。
然后解决原问题需要套一个 dp of dp,把 \(g_i\) 压入状态。
但 \(g_i\) 的信息量太大,需要压缩。
对于这种转移系数极差很小的情况,可以猜测 \(g_i\) 的极差也很小。
因此定义 \(g'_{i,j}=g_{i,j}-\min_k g_{i,k}\),打表发现:\(g'_i\) 的形式和转移系数完全相似,都形如 \(1,2,1,0,1,2,1\)。
即某种奇偶性的下表上全是 \(1\),另一种上是若干个 \(2\) 加若干个 \(0\) 再加若干个 \(2\),证明可以分讨归纳。
那么记录一个 \(g_i\),只需要 \((\min g_i,l,r,len)\),表示长度为 \(len\),取到 \(\min g_i\) 的下标是 \([l,r]\) 中间和 \(l\) 同奇偶的位置。
此时状态仍然过大,进一步发现: \(>r\) 的元素意义较小,因为转移函数相对单调,较大的位置对 dp 转移几乎没有贡献。
事实上可以证明 \((\min g_i,l,r,len)\iff(\min g_i,l,r,\infty)\),即这两个 \(g_i\) 进行内层 dp 的结果相同,从而优化掉一维状态。
这还不够,我们发现同时记录 \([l,r]\) 的复杂度难以承受,进一步观察可以发现 \((\min g_i,l,r)\iff(\min g_i,l,\infty)+(\min g_i,l\bmod 2,r)-(\min g_i,l\bmod 2,\infty)\)。
即最终内层 dp 的答案等价于这三个内层 dp 的答案相加减的结果。
感性理解就是用 \([l,r]\) 转移得到的 \(g'_{i,j}\) 和 \([l\bmod 2,r),[l,\infty)\) 转移得到的 \(g'_{i,j}\) 中的一个相同,即最优的转移点不可能同时存在于 \(<l\) 和 \(>r\) 的位置。
那么每次转移后立刻分拆状态,转移的时候分讨 \(g\) 形态变化即可。
时间复杂度 \(\mathcal O(n^5m)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int n=130,m=30,MOD=1e9+7;
inline void add(int &x,const int &y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
int pw[35][135],dp[35][135][135],f[135][135][135],g[135][135][135];
//dp[v,i,j]: val[1,v], len=i, max del=j
//f[i,j,l],g[i,j,r]: len=i, g0=j, [L0,R0] = [l,inf]/[0,r]
signed main() {
for(int i=0;i<=m;++i) for(int j=pw[i][0]=1;j<=n;++j) pw[i][j]=1ll*pw[i][j-1]*i%MOD;
dp[0][0][0]=1;
for(int v=1;v<=m;++v) {
memset(f,0,sizeof(f));
memset(g,0,sizeof(g));
g[0][0][0]=1;
for(int i=0;i<=n+1;++i) for(int j=0;j<=i;++j) {
for(int l=0;l<=i;++l) if(f[i][j][l]) add(f[i][j][l&1],MOD-f[i][j][l]); //split -> [0,l]-[0,inf]
for(int l=0;l<=i;++l) if(f[i][j][l]) {
const int w=f[i][j][l];
for(int k=0;i+k<=n;++k) {
const int z=1ll*w*pw[v-1][k]%MOD;
if(l>k) { //can't +0
int nr=k-((l+1)&1); //trans +1, (a+l-k)%2=1
if(nr>=0) add(g[i+k+1][j+2][nr],z); //max a = k-(l&1^1)
else add(g[i+k+1][j+3][0],z); //can't +1
} else add(g[i+k+1][j+1][k-l],z); //a+l<=k
}
}
for(int r=0;r<=i;++r) if(g[i][j][r]) {
const int w=g[i][j][r],l=r&1; //[l,r]
for(int k=0;i+k<=n;++k) {
const int z=1ll*w*pw[v-1][k]%MOD;
if(l>k) { //can't +0 (same)
int nr=k-((l+1)&1);
if(nr>=0) add(g[i+k+1][j+2][nr],z);
else add(g[i+k+1][j+3][0],z);
} else {
//d<=a+r,a+l<=k -> a in [d-r,k-l]
//split to [d-r,inf] + [0,k-l] - [0,inf]
add(g[i+k+1][j+1][k-l],z);
int *nw=f[i+k+1][j+1],*Z=dp[v-1][k];
for(int d=r+(k-l)%2;d<=k;d+=2) if(Z[d]) {
nw[d-r]=(nw[d-r]+1ll*w*Z[d])%MOD;
}
}
}
}
}
for(int i=1;i<=n+1;++i) for(int j=1;j<=i;++j) {
//0-th & i-th element is virtual, j=g[0]
for(int l=0;l<=i;++l) {
add(dp[v][i-1][j-1+(!l?0:(l&1?1:2))],f[i][j][l]);
}
for(int r=0;r<=i;++r) {
add(dp[v][i-1][j-1+(r&1)],g[i][j][r]);
}
}
}
int T; scanf("%d",&T);
for(int N,M;T--;) {
scanf("%d%d",&N,&M);
for(int K=0;K<=(N-1)/2;++K) printf("%d ",dp[M][N][N-2*K]); puts("");
}
return 0;
}
*G. [CF1994H] Fortnite
题目大意
交互器里有底数 \(p\) 和模数 \(m\),每次可以询问字符串 \(s\),得到哈希值 \(h(s)=\sum c(s_i)p^i\bmod m\),其中 \(c(\texttt a)=1,c(\texttt b)=2,\dots,c(\texttt z)=26\),在三次询问内求出 \(p,m\)。
数据范围:\(26<p\le 50,p+1<m\le 2\times 10^9\)。
思路分析
定义 \(v(s)=\sum c(s_i)p^i\),即 \(h(s)\) 取模前的结果。
求出 \(p\) 是简单的,因为 \(h(\texttt {aa})=p+1\bmod m=p+1\),因此可以直接求出 \(p\)。
然后我们要通过两次询问确定 \(m\),一个想法是:利用 \(m\mid v(s)-h(s)\) 挖掘性质,但这样显然难以做到两次询问。
那么假设 \(v(s)-h(s)=km\),如果能找到一个字符串 \(t\) 使得 \(v(t)-h(t)=(k-1)m\),很显然能直接得到答案。
这就要求 \(v(t)\in [v(s)-h(s)-m,v(s)-h(s))\),尝试把限制中的 \(m\) 去掉,注意到 \(m<h(s)\),因此可以钦定 \(v(t)\ge v(s)-2h(s)\),依然满足条件。
转成左开右闭区间,就是 \(v(t)\in(v(s)-1-2h(s),v(s)-1-h(s)]\)。
我们要保证 \(v(s)-1-h(s)-v(t)\in [0,h(s)]\)。
把 \(v(s)-1\) 和 \(h(s)\) 表示成 \(p\) 进制数 \(a,b\),逐位构造 \(v(t)\) 的 \(p\) 进制表示 \(c\):
-
如果 \(a_i>b_i\),取 \(c_i=a_i-b_i\),此时 \(1\le c_i\le a_i\),且最终 \(a-b-c\) 上这一位为 \(0\)。
-
否则向 \(a_{i+1}\) 借位,取 \(c_i=a_i\)(借位前的),那么 \(a-b-c\) 上这一位的贡献为 \(p-b_i\)。
由于 \(a_i\) 至多被借一位,所以 \(b_i>a_{i}-1\),因此取 \(a_i=26\) 时,\(b_i>25>p-b_i\),所以此时 \(p-b_i<b_i\)。
从而每一位的贡献都小于 \(b\) 在对应位的贡献。
那么 \(s\) 取全 \(\mathrm{z}\) 串,并且保证 \(v(s)>m\) 即可。
时间复杂度 \(\mathcal O(1)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int a[15],b[15],c[15],p;
int hsh(string s) {
cout<<"? "<<s<<endl;
int o; cin>>o; return o;
}
ll val(string s) {
reverse(s.begin(),s.end());
ll z=0;
for(char o:s) z=z*p+o-'a'+1;
return z;
}
void solve() {
p=hsh("aa")-1;
string X="zzzzzzzzzz";
int x=hsh(X);
for(int i=0,w=x;i<10;++i) a[i]=(i?26:25),b[i]=w%p,w/=p;
string s;
for(int i=0;i<10;++i) {
if(a[i]>b[i]) c[i]=a[i]-b[i];
else --a[i+1],c[i]=a[i];
s.push_back(c[i]-1+'a');
}
int y=hsh(s);
cout<<"! "<<p<<" "<<(val(X)-x)-(val(s)-y)<<endl;
}
signed main() {
int T; cin>>T;
while(T--) solve();
return 0;
}
Round #38 - 2024.12.16
A. [CF2006D] Iris and Adjacent Products
题目大意
定义一个数组是好的当且仅当可以重排该数组使得邻项乘积 \(\le k\)。
给定 \(a_1\sim a_n\),\(q\) 次询问某个子区间至少要修改多少个元素才是好的。
数据范围:\(n,q\le 10^5,k\le 10^6\)。
思路分析
先考虑什么样的数组是好的,对于所有 \(>\sqrt k\) 的数只能和 \(\le \sqrt k\) 的数放一起,因此对于每个 \(i\),\(\le i\) 的元素数量要超过 \(\ge k/i\) 的元素数量。
注意 \(n=2t+1\) 的时候可以有 \(t+1\) 个 \(\ge k/i\) 的元素和 \(t\) 个 \(\le i\) 的元素,因此出现次数要和 \(n/2\) 取 $\min $。
用莫队或分块等方式维护区间中元素的出现次数。
每次修改一定是把 \(>\sqrt k\) 的元素变成 \(1\),求答案对每个 \(i\) 计算后求最大值。
时间复杂度 \(\mathcal O(n+q(\sqrt n+\sqrt k))\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int n,k,q,a[MAXN];
int lp[325],rp[325],bl[MAXN];
int x[325][1005],y[325][1005],c[1005],d[1005];
void solve() {
cin>>n>>q>>k;
for(int i=1;i<=n;++i) cin>>a[i];
int B=sqrt(n),lim=sqrt(k);
for(int i=1;(i-1)*B+1<=n;++i) {
lp[i]=(i-1)*B+1,rp[i]=min(i*B,n);
memcpy(x[i],x[i-1],sizeof(x[i]));
memcpy(y[i],y[i-1],sizeof(y[i]));
for(int j=lp[i];j<=rp[i];++j) {
bl[j]=i;
if(a[j]<=lim) ++x[i][a[j]];
else ++y[i][k/a[j]];
}
}
for(int l,r;q--;) {
cin>>l>>r;
memset(c,0,sizeof(c));
memset(d,0,sizeof(d));
if(bl[l]==bl[r]) {
for(int j=l;j<=r;++j) {
if(a[j]<=lim) ++c[a[j]];
else ++d[k/a[j]];
}
} else {
for(int j=l;j<=rp[bl[l]];++j) {
if(a[j]<=lim) ++c[a[j]];
else ++d[k/a[j]];
}
for(int j=lp[bl[r]];j<=r;++j) {
if(a[j]<=lim) ++c[a[j]];
else ++d[k/a[j]];
}
for(int j=1;j<=lim;++j) {
c[j]+=x[bl[r]-1][j]-x[bl[l]][j];
d[j]+=y[bl[r]-1][j]-y[bl[l]][j];
}
}
int s=0;
for(int j=1;j<=lim;++j) {
c[j]+=c[j-1],d[j]+=d[j-1];
s=max(s,min((d[j]-c[j]+1)/2,(r-l+1)/2-c[j]));
}
cout<<s<<" ";
}
cout<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
B. [CF2004F] Make a Palindrome
题目大意
对于数组 \(a\),一次操作可以把 \(a_i,a_{i+1}\) 替换成 \(a_i+a_{i+1}\) 或进行逆操作。
定义数组权值和为:令数组回文的最小操作数,求 \(a\) 的每个子区间权值和。
数据范围:\(n\le 2000\)。
思路分析
用这样的方式刻画数组 \(b_1\sim b_m\):有 \(B=\sum b_i\) 个球,球中间有 \(m-1\) 个隔板,第 \(i-1\) 个和第 \(i\) 个隔板之间的距离是 \(b_i\)。
设 \(b_i\) 前缀和为 \(s_i\),那么 \(s_1\sim s_{m-1}\) 即为隔板位置。
那么一次操作就是插入或删除一个隔板,而数组回文当且仅当隔板序列回文。
那么每个隔板都要在对面操作一次,除非其对面的位置恰好有隔板,即 \(s_j=B-s_i\) 时对答案产生 \(-2\) 贡献(如果 \(s_j=s_i=B/2\) 答案 \(-1\))。
容易发现两个和相等的不相邻子区间恰好对答案产生 \(-2\) 贡献,因此扫描线维护 \(a[1,i)\) 中每个区间和的出现次数。
注意相邻的两个子区间对答案产生 \(-1\) 贡献。
时间复杂度 \(\mathcal O(n^2)\)。
代码呈现
#include<bits/stdc++.h>
#include<ext/pb_ds/hash_policy.hpp>
#include<ext/pb_ds/assoc_container.hpp>
using namespace std;
using namespace __gnu_pbds;
const int MAXN=2005;
int a[MAXN];
void solve() {
int n,ans=0;
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i];
for(int i=1;i<=n;++i) ans+=(i-1)*(n-i+1);
gp_hash_table <int,int> cnt;
for(int i=1;i<=n;++i) {
for(int j=i,s=0;j<=n;++j) ans-=cnt[s+=a[j]];
for(int j=i,s=0;j>=1;--j) ++cnt[s+=a[j]];
for(int j=i-1,s=0;j>=1;--j) ++cnt[s+=a[j]];
}
cout<<ans<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
int T; cin>>T;
while(T--) solve();
return 0;
}
C. [CF2003F] Turtle and Three Sequences
题目大意
给定 \(n\) 元素,每个元素有高度、颜色、权值,选出 \(m\) 个元素满足高度递增,颜色不同,且权值和最大。
数据范围 \(n\le 3000,m\le 5\)。
思路分析
这种选出不同颜色的元素的题可以考虑 random-coloring,即给每种颜色随机映射到 \([1,m]\) 中,然后计算此问题的答案。
此时可以状压 dp,\(f_{i,s}\) 表示选择了元素 \(i\),出现过的颜色集合为 \(s\) 时的最大权值,树状数组优化转移。
对于最优解,可能被映射到 \(m^m\) 中颜色序列中,其中两两颜色不同的序列有 \(m!\) 个,因此期望随机 \(\dfrac{m^m}{m!}\) 次即可。
时间复杂度 \(\mathcal O\left(2^mn\log n\times\dfrac{m^m}{m!}\right)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3005,inf=1e9,T=400;
mt19937 rnd(time(0));
int n,m,a[MAXN],b[MAXN],c[MAXN],o[MAXN];
struct FenwickTree {
int tr[MAXN],s;
void init() { memset(tr,-0x3f,sizeof(tr)); }
void upd(int x,int v) { for(;x<=n;x+=x&-x) tr[x]=max(tr[x],v); }
int qry(int x) { for(s=-inf;x;x&=x-1) s=max(s,tr[x]); return s; }
} F[32];
signed main() {
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i) scanf("%d",&a[i]);
for(int i=1;i<=n;++i) scanf("%d",&b[i]);
for(int i=1;i<=n;++i) scanf("%d",&c[i]);
int ans=-1;
for(int _=0;_<T;++_) {
for(int s=0;s<(1<<m);++s) F[s].init();
F[0].upd(1,0);
for(int i=1;i<=n;++i) o[i]=rnd()%m;
for(int i=1;i<=n;++i) {
for(int s=0;s<(1<<m);++s) if(!(s>>o[b[i]]&1)){
int z=F[s].qry(a[i])+c[i];
F[s|1<<o[b[i]]].upd(a[i],z);
}
}
ans=max(ans,F[(1<<m)-1].qry(n));
}
printf("%d\n",ans);
return 0;
}
D. [CF2006E] Iris's Full Binary Tree
题目大意
定义一棵树的权值为最小的 \(k\) 使得该树是高度为 \(k\) 的满二叉树的导出子图。
给一棵 \(n\) 个点的树,对于 \(i=1\sim n\),求 \(1\sim i\) 点的导出子图的权值。
数据范围:\(n\le 5\times 10^5\)。
思路分析
刻画一棵树的权值,显然这棵树过二叉树的根,且不能有 \(\ge4\) 度点。
我们发现只要根节点是 \(\le 2\) 度点,其他点度数无限制。
显然根不可能是 \(1\) 度点因此一棵树的权值就是所有 \(2\) 度点到其他点距离最大值的最小值。
取出树的中心,容易发现最优的 \(2\) 度点就是离中心最近的一个。
那么在树上不断加点,中心每次至多移动 \(0.5\) 的距离。
每次中心移动都是对某个子树内或子树外的距离 \(\pm 1\),可以 dfn 序上线段树维护,对于未加入的点以及 \(3\) 度点,直接将距离设成 \(\infty\),加入该点的时候直接求出距离即可。
此时答案就是线段树上的最小值。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5,inf=1e9;
int n;
struct SegmentTree {
int tr[MAXN<<2],tg[MAXN<<2];
void psu(int p) { tr[p]=min(tr[p<<1],tr[p<<1|1]); }
void adt(int p,int k) { tg[p]+=k,tr[p]+=k; }
void psd(int p) { adt(p<<1,tg[p]),adt(p<<1|1,tg[p]),tg[p]=0; }
void init(int l=1,int r=n,int p=1) {
tr[p]=inf,tg[p]=0;
if(l==r) return ;
int mid=(l+r)>>1;
init(l,mid,p<<1),init(mid+1,r,p<<1|1);
}
void set(int u,int k,int l=1,int r=n,int p=1) {
if(l==r) return tr[p]=k,void();
int mid=(l+r)>>1; psd(p);
u<=mid?set(u,k,l,mid,p<<1):set(u,k,mid+1,r,p<<1|1);
psu(p);
}
void add(int ul,int ur,int k,int l=1,int r=n,int p=1) {
if(ul>ur) return ;
if(ul<=l&&r<=ur) return adt(p,k);
int mid=(l+r)>>1; psd(p);
if(ul<=mid) add(ul,ur,k,l,mid,p<<1);
if(mid<ur) add(ul,ur,k,mid+1,r,p<<1|1);
psu(p);
}
} T;
vector <int> G[MAXN];
int fa[MAXN],L[MAXN],R[MAXN],dep[MAXN],st[MAXN][20],dcnt;
void dfs(int u) {
dep[u]=dep[fa[u]]+1,L[u]=++dcnt,st[dcnt][0]=fa[u];
for(int v:G[u]) dfs(v);
R[u]=dcnt;
}
int bit(int x) { return 1<<x; }
int cmp(int x,int y) { return L[x]<L[y]?x:y; }
int LCA(int x,int y) {
if(x==y) return x;
int l=min(L[x],L[y])+1,r=max(L[x],L[y]),k=__lg(r-l+1);
return cmp(st[l][k],st[r-bit(k)+1][k]);
}
int dis(int x,int y) { return dep[x]+dep[y]-2*dep[LCA(x,y)]; }
int nxt(int x,int y) {
if(L[x]<=L[y]&&L[y]<=R[x]) {
return *--upper_bound(G[x].begin(),G[x].end(),y,[&](int i,int j){ return L[i]<L[j]; });
}
return fa[x];
}
int u,v,x,y,deg[MAXN];
void solve() {
cin>>n;
for(int i=1;i<=n;++i) G[i].clear(),deg[i]=0;
for(int i=2;i<=n;++i) cin>>fa[i],G[fa[i]].push_back(i);
dcnt=0,dfs(1);
for(int k=1;k<20;++k) for(int i=1;i+bit(k)-1<=n;++i) {
st[i][k]=cmp(st[i][k-1],st[i+bit(k-1)][k-1]);
}
u=v=x=y=1,T.init(),T.set(1,0),cout<<"1 ";
for(int p=2;p<=n;++p) {
++deg[fa[p]],++deg[p];
T.set(L[p],min(dis(p,x),dis(p,y)));
if(deg[fa[p]]>3) {
for(int i=p;i<=n;++i) cout<<"-1 ";
cout<<"\n";
return ;
}
if(deg[fa[p]]==3) T.set(L[fa[p]],inf);
if(dis(u,p)<dis(v,p)) swap(u,v),swap(x,y);
if(dis(u,p)>dis(u,v)) {
if(x!=y) {
if(fa[x]==y) T.add(L[x],R[x],1);
else T.add(1,L[y]-1,1),T.add(R[y]+1,n,1);
x=y;
} else {
y=nxt(y,p);
if(fa[y]==x) T.add(L[y],R[y],-1);
else T.add(1,L[x]-1,-1),T.add(R[x]+1,n,-1);
}
v=p;
}
cout<<T.tr[1]+(dis(u,v)+1)/2+1<<" ";
}
cout<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
int _; cin>>_;
while(_--) solve();
return 0;
}
*E. [CF1870G] MEXanization
题目大意
定义一个多重集 \(S\) 的权值为:每次选取一个 \(S\) 的子集 \(T\),删除 \(T\) 并加入 \(\mathrm{mex}(T)\),所能得到的最大元素。
给定 \(a_1\sim a_n\),求出 \(a\) 的每个前缀的权值。
数据范围:\(n\le 2\times 10^5\)。
思路分析
发现当 \(i>1\) 的时候,\(a[1,i]\) 的权值单调递增,可以双指针,因此只要判定 \(S\) 的权值是否 \(\ge k\)。
这要求我们有 \(0\sim k-1\) 至少 \(1\) 个。
然后考虑 \(k-1\),如果没有 \(k-1\),我们就需要 \(0\sim k-2\) 至少两个。
然后重复类似的过程,最后判断 \(0\) 的个数够不够。
形式化的来说,记 \(i\) 的出现次数为 \(c_i\),动态维护当前需要的变量个数 \(x\)。
初始 \(x=1\),\(i\) 从 \(k-1\) 枚举到 \(1\):如果 \(c_i<k\),那么 \(k\gets k+(k-c_i)\),如果 \(k<c_i\),那么多出来的 \(c_i-k\) 个元素可以一次操作后变成 \(0\),最后判断是否有 \(c_0\ge k\)。
注意到如果 \(c_i<k\),那么我们至少的元素数量至少增加 \(i-1\),因此想要合法,\(c_i<k\) 的 \(i\) 至多 \(\mathcal O(\sqrt n)\) 个。
可以线段树优化找元素的过程,做到 \(\mathcal O(n \sqrt n\log n)\)。
但实际上能做到更优,考虑在线段树上一边 dfs 一边处理,如果 \([l,r]\) 中最小值 \(\ge k\) 就跳过,如果 \(r\times k>n\) 就返回。
此时第 \(i\) 层至多访问 \(\sqrt{\dfrac n{2^d}}\) 个节点,求和后得到访问总结点量是 \(\mathcal O(\sqrt n)\) 的。
时间复杂度 \(\mathcal O(n\sqrt n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5,inf=2e9;
int n,a[MAXN];
struct SegmentTree {
int su[MAXN<<2],mn[MAXN<<2];
void add(int u,int l=0,int r=n,int p=1) {
++su[p];
if(l==r) return ++mn[p],void();
int mid=(l+r)>>1;
u<=mid?add(u,l,mid,p<<1):add(u,mid+1,r,p<<1|1);
mn[p]=min(mn[p<<1],mn[p<<1|1]);
}
void qry(int ul,int ur,int &k,int &z,int s,int l=0,int r=n,int p=1) {
if(ul<=l&&r<=ur) {
if(k>s/r) return k=inf,void();
if(mn[p]>=k) return z+=su[p]-(r-l+1)*k,void();
if(l==r) {
if(k<mn[p]) z+=mn[p]-k;
else k+=k-mn[p];
return ;
}
}
int mid=(l+r)>>1;
if(mid<ur) qry(ul,ur,k,z,s,mid+1,r,p<<1|1);
if(ul<=mid) qry(ul,ur,k,z,s,l,mid,p<<1);
}
int qs(int ul,int ur,int l=0,int r=n,int p=1) {
if(ul<=l&&r<=ur) return su[p];
int mid=(l+r)>>1,s=0;
if(ul<=mid) s+=qs(ul,ur,l,mid,p<<1);
if(mid<ur) s+=qs(ul,ur,mid+1,r,p<<1|1);
return s;
}
} T;
void solve() {
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i];
int x=0;
for(int i=1;i<=n;++i) {
T.add(min(a[i],n));
for(;x<i;++x) {
int k=1,z=T.qs(x+1,n)+T.qs(0,0);
if(x) T.qry(1,x,k,z,i);
if(z<k) break;
}
cout<<(i==1?max(a[i],x):x)<<" ";
}
cout<<"\n";
for(int i=1;i<=(n+1)*4;++i) T.su[i]=T.mn[i]=0;
}
signed main() {
ios::sync_with_stdio(false);
int _; cin>>_;
while(_--) solve();
return 0;
}
*F. [CF1896H2] Cyclic Hamming
题目大意
给定 \(k\),定义两个长度为 \(2^{k+1}\) 的 01 串 \(a,b\) 是好的当且仅当:
- \(a,b\) 分别含有 \(2^k\) 个 \(1\)。
- 对于每个 \(b\) 的循环移位 \(b'\),\(\mathrm{dis}(a,b')\ge 2^k\),\(\mathrm{dis}\) 表示海明距离。
给定含有未确定字符的字符串 \(a,b\),求所有填充方式中有多少使得 \((a,b)\) 是好的。
数据范围:\(k\le 12\)。
思路分析
记 \(n=2^k\)。
首先 \(\sum \mathrm{dis}(a,b')=2^{k+1}2^k\),而 \(b'\) 恰好有 \(2^{k+1}\) 个,因此题目限制等价于 \(\mathrm{dis}(a,b')=2^k\)。
用卷积刻画循环移位的海明距离,首先由于 \(a_i=b'_i=0\) 的位置数量和 \(a_i=b'_i=1\) 的位置数量相等,因此海明距离 \(=2^k\) 等价于 \(a_i=b'_i=1\) 的位置有 \(2^{k-1}\) 个。
然后构造两个多项式 \(A=\sum a_ix^i,B=\sum b_{2n-x-1}x^i\),每个循环移位的海明距离就是 \(A\times B\bmod{(x^{2n}-1)}\) 的每一项。
因此题目限制就是 \(A\times B \bmod{(x^{2n}-1)}=2^{k-1}(1+\dots+x^{2n-1})\)。
用减法代替多项式取模,即 \(\exist C\) 使得 \(A\times B=C(x^{2n}-1)+2^{k-1}(1+\dots +x^{2n-1})\)。
注意到右式有公因式:\(A\times B=((x-1)C+2^{k-1})(1+\dots+x^{2n-1})=((x-1)C+2^{k-1})\prod_{i=0}^k(1+x^{2^i})\)。
先要求 \((1+x^{2^i})\mid A\times B\),然后就是 \(\dfrac{A\times B}{\prod (1+x^{2^i})}=(x-1)C+2^{k-1}\),即 \(\dfrac{A\times B}{\prod (1+x^{2^i})}\bmod{(x-1)}=2^{k-1}\)。
相当于要求带入 \(x=1\) 后,\(\dfrac{A(1)\times B(1)}{2^{k+1}}=2^{k-1}\),由于 \(A(1)=B(1)=2^k\),这是显然成立的。
因此合法的充要条件就是所有 \(1+x^{2^i}\mid A\) 或 \(1+x^{2^i}\mid B\)。
刻画这个条件,对 \(A\) 的系数按 \(\bmod {2^i}\) 的余数分类,每部分独立,且都要求是 \(1+x\) 的倍数。
也就是说,把 \(A\) 的系数按 \(\bmod 2^i\) 分类,得到的每个多项式带入 \(x=-1\) 结果为 \(0\)。
用 Trie 树分类,把 \(a_p=1\) 的 \(p\) 二进制位倒着插入一棵 Trie,\(\bmod 2^i\) 分类相当于第 \(i\) 层的每棵子树 \(u\)。
带入 \(x=-1\) 等价于及奇数项和等于偶数项和,注意到奇数项为 \(u\) 的 \(1\) 儿子,偶数项为 \(u\) 的 \(0\) 儿子。
因此 \(1+x^{2^i}\mid A\) 就要求 Trie 树上深度为 \(i\) 的每个节点左右子树中叶子个数相等。
那么对于 \(A,B\) 分别枚举整除哪些 \(1+x^{2^i}\),在 Trie 树上自下而上 dp,\(f_{u,s,i}\) 表示 Trie 上 \(u\) 子树中整除 \(s\) 里的多项式,包含 \(i\) 个叶子的方案数。
暴力转移复杂度 \(\mathcal O(8^k)\),且状态数过高。
用观察优化,设 \(u\) 子树深度为 \(d\),那么 \(s\) 只有 \(2^d\) 种可能,且注意到 \(|s|\ge 1\) 的时候 \(j\) 一定是偶数,进一步,我们知道 \(2^{|s|}\mid j\),因此 \(d\) 只有 \(2^{d-|s|}\) 种。
那么枚举 \(|s|=i\),状态数 \(\sum_d 2^{k-d}\sum_{i\le d}\binom{d}{i}2^i=\mathcal O(3^k)\),转移的时候要对左右子树大小卷积,复杂度 \(\sum_d 2^{k-d}\sum_{i\le d}\binom{d}{i}2^{2i}=\mathcal O(5^k)\)。
如果用 NTT 优化左右子树的卷积,复杂度可以做到 \(\mathcal O(k3^k)\)。
时间复杂度 \(\mathcal O(5^k)\)。
代码呈现
#include<bits/stdc++.h>
#define pc __builtin_popcount
#define ll long long
using namespace std;
const int MOD=998244353,MAXN=(1<<13)+5;
vector<vector<int>> f[14][MAXN];
//f[i,s,u,j]: dep = i, ok dig = s, node u, size = j*2^|s|
int n,k,rv[MAXN];
inline void add(int &x,const ll &y) { x=(x+y)%MOD; }
void DP(char *str,int *dp) {
for(int i=1;i<n;++i) rv[i]=(rv[i>>1]>>1)|(i&1?n>>1:0);
for(int i=0;i<n;++i) if(rv[i]>i) swap(str[i],str[rv[i]]);
for(int i=0;i<=k+1;++i) for(int s=0;s<(1<<i);++s) {
int U=1<<(k+1-i),J=1<<(i-pc(s));
f[i][s]=vector<vector<int>>(U,vector<int>(J+1,0));
}
for(int i=0;i<n;++i) {
f[0][0][i][0]=(str[i]!='1');
f[0][0][i][1]=(str[i]!='0');
}
for(int i=1;i<=k+1;++i) for(int s=0;s<(1<<(i-1));++s) {
int U=1<<(k+1-i),J=1<<(i-pc(s)-1);
for(int u=0;u<U;++u) {
auto &fl=f[i-1][s][u<<1],&fr=f[i-1][s][u<<1|1];
for(int x=0;x<=J;++x) {
add(f[i][s|(1<<(i-1))][u][x],1ll*fl[x]*fr[x]);
}
auto &fi=f[i][s][u];
for(int x=0;x<=J;++x) if(fl[x]) for(int y=0;y<=J;++y) if(fr[y]) {
add(fi[x+y],1ll*fl[x]*fr[y]);
}
}
}
for(int s=0;s<n-1;++s) dp[s]=f[k+1][s][0][1<<(k-pc(s))];
}
char S[MAXN],T[MAXN];
int fs[MAXN],ft[MAXN];
signed main() {
scanf("%d%s%s",&k,S,T),n=1<<(k+1);
reverse(T,T+n);
DP(S,fs),DP(T,ft);
for(int i=0;i<=k;++i) for(int s=0;s<n;++s) if(!(s>>i&1)) fs[s]=(fs[s]+MOD-fs[s|1<<i])%MOD;
int ans=0;
for(int s=0;s<n;++s) add(ans,1ll*fs[s]*ft[(n-1)^s]);
printf("%d\n",ans);
return 0;
}
Round #39 - 2024.12.18
A. [CF2009G3] Yunli's Subarray Queries
题目大意
给定 \(n,k\),定义序列权值为至少要修改几个位置才能使得其中存在一个长度 \(\ge k\) 的公差为 \(1\) 的等差数列。
给定 \(a_1\sim a_n\),\(q\) 次询问 $$求这个值的和。
数据范围:\(n\le 2\times 10^5\)。
思路分析
先计算每个长度为 \(k\) 的区间的答案,相当于 \(a_i-i\) 的众数,计算是简单的。
记 \(b_i=a[i,i+k-1]\) 的答案,那么一个区间的答案就是 \(b_i\) 的区间最小值。
因此答案就是 \(b_i\) 每个子区间最小值的和,这是经典问题,扫描线单调栈,配合历史和线段树维护之即可。
时间复杂度 \(\mathcal O((n+q)\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,m,q,a[MAXN];
struct SegmentTree {
int len[MAXN<<2];
ll sum[MAXN<<2],sumh[MAXN<<2],ad[MAXN<<2],ti[MAXN<<2],sd[MAXN<<2];
void trs(int p) {
sumh[p]+=sum[p],sd[p]+=ad[p],++ti[p];
}
void adt(int p,ll k) {
sum[p]+=k*len[p],ad[p]+=k;
}
void psd(int p) {
for(int c:{p<<1,p<<1|1}) {
sumh[c]+=sum[c]*ti[p]+len[c]*sd[p];
sd[c]+=ad[c]*ti[p]+sd[p];
sum[c]+=ad[p]*len[c],ad[c]+=ad[p],ti[c]+=ti[p];
}
ad[p]=ti[p]=sd[p]=0;
}
void psu(int p) { sum[p]=sum[p<<1]+sum[p<<1|1],sumh[p]=sumh[p<<1]+sumh[p<<1|1]; }
void init(int l=1,int r=n,int p=1) {
len[p]=r-l+1,sum[p]=sumh[p]=ad[p]=ti[p]=sd[p]=0;
if(l==r) return ;
int mid=(l+r)>>1; init(l,mid,p<<1),init(mid+1,r,p<<1|1);
}
void add(int ul,int ur,ll k,int l=1,int r=n,int p=1) {
if(ul<=l&&r<=ur) return adt(p,k);
int mid=(l+r)>>1; psd(p);
if(ul<=mid) add(ul,ur,k,l,mid,p<<1);
if(mid<ur) add(ul,ur,k,mid+1,r,p<<1|1);
psu(p);
}
ll qry(int ul,int ur,int l=1,int r=n,int p=1) {
if(ul<=l&&r<=ur) return sumh[p];
int mid=(l+r)>>1; ll ans=0; psd(p);
if(ul<=mid) ans+=qry(ul,ur,l,mid,p<<1);
if(mid<ur) ans+=qry(ul,ur,mid+1,r,p<<1|1);
return ans;
}
} T;
int c[MAXN*2],w[MAXN],mx,v[MAXN];
void add(int x) {
--w[c[x]],++w[++c[x]];
if(w[mx+1]) ++mx;
}
void del(int x) {
--w[c[x]],++w[--c[x]];
if(!w[mx]) --mx;
}
int stk[MAXN];
ll ans[MAXN];
vector <array<int,2>> Q[MAXN];
void solve() {
cin>>n>>m>>q,mx=0,w[0]=n;
for(int i=1;i<=n;++i) cin>>a[i],a[i]=a[i]-i+n;
for(int i=1;i<=m;++i) add(a[i]);
v[1]=m-mx;
for(int i=2;i<=n-m+1;++i) {
del(a[i-1]),add(a[i+m-1]),v[i]=m-mx;
}
for(int i=1,l,r;i<=q;++i) {
cin>>l>>r,Q[r-m+1].push_back({l,i});
}
T.init();
for(int i=1,tp=0;i<=n-m+1;++i) {
for(;tp&&v[stk[tp]]>=v[i];--tp) T.add(stk[tp-1]+1,stk[tp],-v[stk[tp]]);
T.add(stk[tp]+1,i,v[i]),stk[++tp]=i;
T.trs(1);
for(auto o:Q[i]) ans[o[1]]=T.qry(o[0],i);
}
for(int i=1;i<=q;++i) cout<<ans[i]<<"\n";
for(int i=0;i<=n*2;++i) c[i]=0;
for(int i=0;i<=n;++i) w[i]=0,Q[i].clear();
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
B. [CF2013F2] Digital Village
题目大意
给定 \(n\) 个点的树,先手从 \(1\) 出发,后手从 \(x\) 出发,两个人轮流在树上游走,不能重复经过点,对于 \(u\to v\) 上的每个 \(x\),求谁有必胜策略。
数据范围:\(n\le 2\times 10^5\)。
思路分析
假设 \(1\to x\) 路径是 \(p_1\sim p_m\),两个人的策略就是在 \(p\) 上运动,直到某个时刻必胜的时候离开路径。
设 \(d_i\) 表示从 \(p_i\) 处离开路径,最多能走多少步。
假设先手在 \(p_l\) 处离开路径必胜,这就要求:\(\max_{i=l+1}^{m-l+1}m-i+d_i<d_l+l-1\)。
假设后手在 \(p_r\) 处离开路径必胜,这就要求:\(\max_{i=m-r+2}^{r-1}a_i+i-1\le a_r+m-r\)。
暴力枚举每个 \(l,r\),考虑第一个合法的 \(l\) 和第一个合法的 \(r\),比较 \(l-1,n-r\) 即可知道谁必胜,现在要优化这部分的分之道。
定义 \(f_l=d_l+l-1,g_r=a_r+m-r\),表示先手 / 后手从 \(p_l,p_r\) 处离开路径后最多能走多远。
我们要找 \(f_l>\max g[l+1,m-l+1]\) 的第一个 \(l\),我们发现随着 \(l\) 的增加,\(\max g\) 的范围越来越小,因此 \(i<j,f_i<f_j\) 时,\(i\) 必胜能推出 \(j\) 必胜 。
注意到 \(l\) 变化的时候 \(\max g\) 实际上不太容易变化,可以找到 \(g[mid+1,m]\) 中的最大值 \(g_r\),只有 \(l-1>n-r\) 的时候才会产生变化。
找到 \(f[1,mid]\) 的最大值 \(f_l\),不妨设 \(l-1\le n-r\),比较 \(f_l,g_r\) 的关系:
- 如果 \(f_l\le g_r\),那么 \(i\in [1,l]\) 的点都不可能合法。
- 否则 \(i\in [n-l+1,n]\) 的点都不可能合法,我们只要判断 \(i\in [1,l]\) 中有没有合法点,很显然最容易合法的就是 \(f_l\),只需要判断 \(f_l\) 是否合法即可。
因此我们只需要判定 \(f_l\) 是否合法,然后就把问题缩小成了 \([l,n-l+1]\) 的子问题。
不断这样的递归,我们会需要判定的 \(l\) 一定是 \(f[1,mid]\) 的后缀最大值,需要判定的 \(r\) 一定是 \(g[mid+1,r]\) 的前缀最大值。
考虑 \(i<j,f_i\ge f_j\) 的两个点,此时一定有 \(d_i>d_j\),因此 \(f[1,mid]\) 的后缀最大值的 \(d\) 单调递增。
由于 \(\sum d_i=\mathcal O(n)\),因此访问到的 \(l,r\) 总数是 \(\mathcal O(\sqrt n)\) 级别的。
在 dfs 的时候动态维护栈中节点的 \(f_i,g_i\),需要一个数据结构支持修改末尾元素,快速查询区间最大值。
可以用 st 表,\(s_{i,k}\) 维护 \([i-2^k+1,i]\) 范围内的最小值,修改末尾元素 \(\mathcal O(\log n)\),查询最大值 \(\mathcal O(1)\)。
此时我们能得到后手从每个 \(x\) 出发是否必胜,容易回答询问。
时间复杂度 \(\mathcal O(n\sqrt n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
inline int bit(int x) { return 1<<x; }
struct ST {
int v[MAXN],f[MAXN][18];
int cmp(int x,int y) { return v[x]>v[y]?x:y; }
void upd(int x,int z) {
v[x]=z,f[x][0]=x;
for(int k=1;bit(k)<=x;++k) f[x][k]=cmp(f[x][k-1],f[x-bit(k-1)][k-1]);
}
int qry(int l,int r) {
int k=__lg(r-l+1);
return cmp(f[l+bit(k)-1][k],f[r][k]);
}
} A,B;
void upd(int d,int w) { A.upd(d,w+d-1),B.upd(d,w-d); }
bool sol(int n) {
int mid=(n+1)>>1;
for(int l=A.qry(1,mid),r=B.qry(mid+1,n);;) {
if(l-1<=n-r) {
if(A.v[l]>B.v[B.qry(l+1,n-l+1)]+n) return 1;
if(l>=mid) return 0;
l=A.qry(l+1,mid);
} else {
if(B.v[r]+n>=A.v[A.qry(n-r+2,r-1)]) return 0;
if(r<=mid+1) return 1;
r=B.qry(mid+1,r-1);
}
}
}
vector <int> G[MAXN];
int n,d[MAXN],f[MAXN],fa[MAXN];
bool ans[MAXN];
void dfs1(int u,int fz) {
d[u]=d[fz]+1,f[u]=0,fa[u]=fz;
for(int v:G[u]) if(v^fz) dfs1(v,u),f[u]=max(f[u],f[v]+1);
}
void dfs2(int u) {
int mx=0,sx=0;
for(int v:G[u]) if(v^fa[u]) {
if(f[v]+1>mx) sx=mx,mx=f[v]+1;
else sx=max(sx,f[v]+1);
}
if(u>1) upd(d[u],mx),ans[u]=sol(d[u]);
for(int v:G[u]) if(v^fa[u]) upd(d[u],f[v]+1==mx?sx:mx),dfs2(v);
}
void prt(int u) { cout<<(ans[u]?"Alice":"Bob")<<"\n"; }
void out(int u,int v) {
if(u==v) return prt(u);
if(d[u]>d[v]) prt(u),out(fa[u],v);
else out(u,fa[v]),prt(v);
}
void solve() {
cin>>n;
for(int i=1;i<=n;++i) G[i].clear();
for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
dfs1(1,0),dfs2(1);
int s,t;
cin>>s>>t,out(s,t);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
C. [CF2020F] Count Leaves
题目大意
给定 \(n,d\),构造一棵深度为 \(d\) 的有根树,根节点的标号为 \(n\),标号为 \(x\) 的节点有 \(\sigma_0(x)\) 个儿子,标号为分别为 \(x\) 的每个因数。
设 \(f(n,d)\) 为这棵树的叶子个数,求 \(\sum_{i=1}^n f(i^k,d)\)。
数据范围:\(n\le 10^9,k,d\le 10^5\)。
思路分析
先刻画 \(f(n,d)\),观察 \(n\) 的某个约数 \(w\) 出现次数,相当于 \(n\) 的每个质因子的质数逐层递减,计数路径条数,
容易发现 \(f(n,d)\) 是积性函数,因此只要考虑 \(f(p^c,d)\),其中 \(p\) 是质数,很显然 \(f(p^c,d)\) 就是 \(\binom{c+d}{d}\)。
因此 \(w(i)=f(i^k,d)=\prod_{p^c}\binom{ck+d}d\),要算的就是这个积性函数的答案。
从小大大爆搜每个质因数,枚举 \(i\) 除以最大质因子的结果,如果最大质因子出现次数 \(=1\),贡献确定,只要数质数个数,相当于数 \(1\sim \left\lfloor\dfrac nt\right\rfloor\) 的质数个数,Min25 筛预处理。
而最大质因子出现次数 \(\ge 2\),此时这个质因子 \(\le \sqrt n\),在爆搜的时候特判即可。
时间复杂度 \(\mathcal O\left(\dfrac{n^{3/4}}{\log n}\right)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5,MOD=1e9+7,MAXV=3.2e6+5;
ll fac[MAXV],ifac[MAXV];
ll ksm(ll a,ll b=MOD-2) { ll s=1; for(;b;a=a*a%MOD,b>>=1) if(b&1) s=s*a%MOD; return s; }
ll C(int x,int y) {
if(x<0||y<0||y>x) return 0;
return fac[x]*ifac[y]%MOD*ifac[x-y]%MOD;
}
int n,K,D,B,val[MAXN];
bool isc[MAXN];
int p[MAXN],tot,m,idx1[MAXN],idx2[MAXN],g[MAXN];
inline int idx(int v) {
return (v<=B)?idx1[v]:idx2[n/v];
}
ll f[32],ans;
void dfs(int i,int N,ll dp) {
if(g[idx(N)]>i) ans=(ans+dp*f[1]%MOD*(g[idx(N)]-i))%MOD;
for(int j=i+1;j<=tot&&p[j]<=N/p[j];++j) {
for(int c=1,M=N/p[j];M>=p[j];++c,M/=p[j]) {
ans=(ans+dp*f[c+1])%MOD;
dfs(j,M,dp*f[c]%MOD);
}
}
}
void solve() {
scanf("%d%d%d",&n,&K,&D),B=sqrt(n),tot=m=0;
for(int i=2;i<=B;++i) {
if(!isc[i]) p[++tot]=i;
for(int j=1;j<=tot&&i*p[j]<=B;++j) {
isc[i*p[j]]=true;
if(i%p[j]==0) break;
}
}
for(ll l=1,r;l<=n;l=r+1) {
r=n/(n/l),val[++m]=n/l;
if(val[m]<=B) idx1[val[m]]=m;
else idx2[n/val[m]]=m;
g[m]=val[m]-1;
}
for(int k=1;k<=tot;++k) {
for(int i=1;i<=m&&1ll*p[k]*p[k]<=val[i];++i) {
g[i]-=g[idx(val[i]/p[k])]-(k-1);
}
}
for(int i=1;i<=30;++i) f[i]=C(K*i+D,D);
ans=1,dfs(0,n,1);
printf("%lld\n",ans);
for(int i=1;i<=m;++i) val[i]=g[i]=0;
for(int i=1;i<=B;++i) idx1[i]=idx2[i]=0,isc[i]=false;
}
signed main() {
for(int i=fac[0]=1;i<MAXV;++i) fac[i]=fac[i-1]*i%MOD;
ifac[MAXV-1]=ksm(fac[MAXV-1]);
for(int i=MAXV-1;i;--i) ifac[i-1]=ifac[i]*i%MOD;
int _; scanf("%d",&_);
while(_--) solve();
return 0;
}
*D. [CF2018E2] Complex Segments
题目大意
给定 \(n\) 个区间,选出若干个区间,使得其可以分割成若干个大小相同的子集,且两个区间相交当且仅当他们属于同一个子集,最大化选出区间个数。
数据范围:\(n\le 3\times 10^5\)。
思路分析
一个子集中的线段两两有交,相当于有一个公共点。
很自然的想法是枚举每个子集的大小 \(k\),算出能分出多少个组 \(f_k\)。
一种求 \(f_k\) 的方法如下:按右端点从小到大考虑每条线段,数据结构维护每个点被覆盖的次数,如果存在一个点被覆盖 \(\ge k\) 次,那么就把 \(l_j\le r_i\) 的线段全部删除。
此时我们得到了 \(\mathcal O(n\log n)\) 求单个 \(f_k\) 的算法。
注意到 \(f_k\le \dfrac nk\) 并且 \(f_k\) 单调递减,于是可以整体二分,在 \(f_l=f_r\) 的时候直接得到返回。
此时考虑第 \(d\) 层的递归次数:前 \(2^{d/2}\) 个区间每个查询一次,剩余的区间左端点至少是 \(\dfrac{n}{2^d}\times 2^{d/2}\),那么 \(f_i\le 2^{d/2}\),这部分的查询次数也是 \(2^{d/2}\) 的。
那么总的查询次数就是 \(\sum 2^{d/2}=\mathcal O(\sqrt n)\)。
直接计算可以做到 \(\mathcal o(n\sqrt n\log n)\)。
需要优化计算 \(f_k\) 的复杂度,需要解决的问题就是扫描线的时候后缀加,查询全局最大值,很显然我们只关心后缀最大值。
首先我们可以离散化,使得所有区间的左右端点互不重合,那么后缀最大值每次变化量为 \(\{0,1\}\)。
给 \([l,r]\) 后缀加的时候就是找 \(<l\) 的最后一个后缀最大值 \(+1\) 的位置,并把把差分值改成 \(+0\)。
如果 \(+1\) 不存在说明全局最大值 \(+1\),用并查集就能维护这个过程。
时间复杂度 \(\mathcal O(n\alpha(n)\sqrt n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=6e5+5;
array<int,2> a[MAXN];
int n,dsu[MAXN],ans;
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
int f(int k) {
iota(dsu,dsu+2*n+1,0);
int s=0,lim=0,lst=0,cnt=0;
for(int i=1;i<=n;++i) {
int l=a[i][0],r=a[i][1];
if(l<=lim) continue;
for(int u=lst+1;u<r;++u) dsu[u]=lst;
int p=find(l); lst=r;
if(p<=lim) ++cnt;
else dsu[p]=p-1;
if(cnt>=k) cnt=0,lim=r,++s;
}
ans=max(ans,s*k);
return s;
}
void cdq(int l,int r,int lo,int hi) {
if(l>r||lo==hi) return ;
int mid=(l+r)>>1,z=f(mid);
cdq(l,mid-1,lo,z),cdq(mid+1,r,z,hi);
}
void solve() {
cin>>n;
vector <array<int,2>> st;
for(int i=1;i<=n;++i) cin>>a[i][0],st.push_back({a[i][0],-i});
for(int i=1;i<=n;++i) cin>>a[i][1],st.push_back({a[i][1],i});
sort(st.begin(),st.end());
for(int i=0;i<2*n;++i) {
if(st[i][1]<0) a[-st[i][1]][0]=i+1;
else a[st[i][1]][1]=i+1;
}
sort(a+1,a+n+1,[&](auto x,auto y){ return x[1]<y[1]; });
ans=0,cdq(2,n-1,f(1),f(n));
cout<<ans<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
int _; cin>>_;
while(_--) solve();
return 0;
}
*E. [CF2013F2] Game in Tree
题目大意
给定 \(n\) 个点的树,先手从 \(1\) 出发,后手从 \(x\) 出发,两个人轮流在树上游走,不能重复经过点,对于 \(u\to v\) 上的每个 \(x\),求谁有必胜策略。
数据范围:\(n\le 2\times 10^5\)。
思路分析
假设 \(1\to x\) 路径是 \(p_1\sim p_m\),两个人的策略就是在 \(p\) 上运动,直到某个时刻必胜的时候离开路径。
设 \(d_i\) 表示从 \(p_i\) 处离开路径,最多能走多少步。
假设先手在 \(p_l\) 处离开路径必胜,这就要求:\(\max_{i=l+1}^{m-l+1}m-i+d_i<d_l+l-1\)。
假设后手在 \(p_r\) 处离开路径必胜,这就要求:\(\max_{i=m-r+2}^{r-1}a_i+i-1\le a_r+m-r\)。
暴力枚举每个 \(l,r\),考虑第一个合法的 \(l\) 和第一个合法的 \(r\),比较 \(l-1,n-r\) 即可知道谁必胜,现在要优化这部分的分之道。
定义 \(f_l=d_l+l-1,g_r=a_r+m-r\),表示先手 / 后手从 \(p_l,p_r\) 处离开路径后最多能走多远。
我们要找 \(f_l>\max g[l+1,m-l+1]\) 的第一个 \(l\),我们发现随着 \(l\) 的增加,\(\max g\) 的范围越来越小,因此 \(i<j,f_i<f_j\) 时,\(i\) 必胜能推出 \(j\) 必胜 。
注意到 \(l\) 变化的时候 \(\max g\) 实际上不太容易变化,可以找到 \(g[mid+1,m]\) 中的最大值 \(g_r\),只有 \(l-1>n-r\) 的时候才会产生变化。
找到 \(f[1,mid]\) 的最大值 \(f_l\),不妨设 \(l-1\le n-r\),比较 \(f_l,g_r\) 的关系:
- 如果 \(f_l\le g_r\),那么 \(i\in [1,l]\) 的点都不可能合法。
- 否则 \(i\in [n-l+1,n]\) 的点都不可能合法,我们只要判断 \(i\in [1,l]\) 中有没有合法点,很显然最容易合法的就是 \(f_l\),只需要判断 \(f_l\) 是否合法即可。
因此我们只需要判定 \(f_l\) 是否合法,然后就把问题缩小成了 \([l,n-l+1]\) 的子问题。
不断这样的递归,我们会需要判定的 \(l\) 一定是 \(f[1,mid]\) 的后缀最大值,需要判定的 \(r\) 一定是 \(g[mid+1,r]\) 的前缀最大值。
考虑 \(i<j,f_i\ge f_j\) 的两个点,此时一定有 \(d_i>d_j\),因此 \(f[1,mid]\) 的后缀最大值的 \(d\) 单调递增。
由于 \(\sum d_i=\mathcal O(n)\),因此访问到的 \(l,r\) 总数是 \(\mathcal O(\sqrt n)\) 级别的。
在 dfs 的时候动态维护栈中节点的 \(f_i,g_i\),需要一个数据结构支持修改末尾元素,快速查询区间最大值。
可以用 st 表,\(s_{i,k}\) 维护 \([i-2^k+1,i]\) 范围内的最小值,修改末尾元素 \(\mathcal O(\log n)\),查询最大值 \(\mathcal O(1)\)。
此时我们能得到后手从每个 \(x\) 出发是否必胜,容易回答询问。
时间复杂度 \(\mathcal O(n\sqrt n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
inline int bit(int x) { return 1<<x; }
struct ST {
int v[MAXN],f[MAXN][18];
int cmp(int x,int y) { return v[x]>v[y]?x:y; }
void upd(int x,int z) {
v[x]=z,f[x][0]=x;
for(int k=1;bit(k)<=x;++k) f[x][k]=cmp(f[x][k-1],f[x-bit(k-1)][k-1]);
}
int qry(int l,int r) {
int k=__lg(r-l+1);
return cmp(f[l+bit(k)-1][k],f[r][k]);
}
} A,B;
void upd(int d,int w) { A.upd(d,w+d-1),B.upd(d,w-d); }
bool sol(int n) {
int mid=(n+1)>>1;
for(int l=A.qry(1,mid),r=B.qry(mid+1,n);;) {
if(l-1<=n-r) {
if(A.v[l]>B.v[B.qry(l+1,n-l+1)]+n) return 1;
if(l>=mid) return 0;
l=A.qry(l+1,mid);
} else {
if(B.v[r]+n>=A.v[A.qry(n-r+2,r-1)]) return 0;
if(r<=mid+1) return 1;
r=B.qry(mid+1,r-1);
}
}
}
vector <int> G[MAXN];
int n,d[MAXN],f[MAXN],fa[MAXN];
bool ans[MAXN];
void dfs1(int u,int fz) {
d[u]=d[fz]+1,f[u]=0,fa[u]=fz;
for(int v:G[u]) if(v^fz) dfs1(v,u),f[u]=max(f[u],f[v]+1);
}
void dfs2(int u) {
int mx=0,sx=0;
for(int v:G[u]) if(v^fa[u]) {
if(f[v]+1>mx) sx=mx,mx=f[v]+1;
else sx=max(sx,f[v]+1);
}
if(u>1) upd(d[u],mx),ans[u]=sol(d[u]);
for(int v:G[u]) if(v^fa[u]) upd(d[u],f[v]+1==mx?sx:mx),dfs2(v);
}
void prt(int u) { cout<<(ans[u]?"Alice":"Bob")<<"\n"; }
void out(int u,int v) {
if(u==v) return prt(u);
if(d[u]>d[v]) prt(u),out(fa[u],v);
else out(u,fa[v]),prt(v);
}
void solve() {
cin>>n;
for(int i=1;i<=n;++i) G[i].clear();
for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
dfs1(1,0),dfs2(1);
int s,t;
cin>>s>>t,out(s,t);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
Round #40 - 2024.12.19
A. [CF2032F] Peanuts
题目大意
给定 \(a_1\sim a_n\),先手先将 \(a_1\sim a_n\) 分成若干个连续段,然后两个人从左到右依次在每个连续段内部做 Nim 游戏,求有多少分段策略使得先手必胜。
数据范围:\(n\le 10^6\)。
思路分析
考虑如何判定一个局面是先手必胜还是后手必胜,从最后一个连续段开始,先手肯定要赢下这个游戏。
那么这个游戏如果先手必胜,则先手要输掉前面那个游戏,使得他能在这个游戏中成为先手,否则先手要赢下前面那个游戏。
因此从后往前就能推断出想要获胜的人要在每个游戏中赢还是输。
于是可以 dp,\(f_{i,0/1}\) 表示 \(a[i,n]\) 的分段已经确定,想获胜要输 / 赢 \(a_{i-1}\) 所处的游戏。
转移系数就是 \(a[i,j)\) 构成的游戏先手有没有必胜 / 必败策略。
先手有必胜策略就是 Nim 博弈,即异或和 \(\ne 0\)。
先手有必败策略则是 Anti-Nim 博弈,判定条件是 \(\max a_i>1\) 时异或和 \(\ne 0\),或者所有 \(a_i=1\) 时异或和 \(=0\)。
因此暴力 dp 可以做到 \(\mathcal O(n^2)\)。
优化转移只需要对异或前缀和开桶,特殊讨论 \(\max a[i,j)=1\) 的情况。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5,MOD=998244353;
int n,a[MAXN],s[MAXN];
ll f[MAXN][2],g[MAXN][2];
//f[i][0/1]: fill [i,n], lose/win game at i-1
void solve() {
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i];
vector <int> vals{0};
for(int i=n;i>=1;--i) vals.push_back(s[i]=s[i+1]^a[i]);
sort(vals.begin(),vals.end()),vals.erase(unique(vals.begin(),vals.end()),vals.end());
for(int i=1;i<=n+1;++i) s[i]=lower_bound(vals.begin(),vals.end(),s[i])-vals.begin()+1;
g[s[n+1]][1]=g[0][1]=f[n+1][1]=1;
int h[2]={0,0};
for(int i=n,j=n+1;i>=1;--i) {
if(a[i]>1) {
h[0]=h[1]=0;
for(;j>i;--j) {
g[s[j]][0]=(g[s[j]][0]+f[j][0])%MOD;
g[0][0]=(g[0][0]+f[j][0])%MOD;
}
}
f[i][1]=(f[i][1]+g[s[i]][1])%MOD;
f[i][0]=(f[i][0]+g[0][1]+MOD-g[s[i]][1])%MOD;
f[i][1]=(f[i][1]+g[s[i]][0])%MOD;
f[i][0]=(f[i][0]+g[0][0]+MOD-g[s[i]][0])%MOD;
f[i][1]=(f[i][1]+h[(i+1)&1])%MOD;
f[i][0]=(f[i][0]+h[i&1])%MOD;
g[s[i]][1]=(g[s[i]][1]+f[i][1])%MOD;
g[0][1]=(g[0][1]+f[i][1])%MOD;
h[i&1]=(h[i&1]+f[i][0])%MOD;
}
cout<<f[1][0]<<"\n";
for(int i=0;i<=n+1;++i) {
a[i]=s[i]=f[i][0]=f[i][1]=g[i][0]=g[i][1]=0;
}
}
signed main() {
ios::sync_with_stdio(false);
int T; cin>>T;
while(T--) solve();
return 0;
}
B. [CF2022D2] Asesino
题目大意
给定 \(n\) 个人,每个人是骑士或骗子,骑士总说真话,骗子总说假话,但现在有一个骗子进行了伪装,使得其他人都认为他是骑士。
你可以询问第 \(i\) 个人是否认为第 \(j\) 个人是骑士,在最少次数内确定伪装的骗子。
数据范围:\(n\le 10^5\)。
思路分析
询问 \(i,j\) 相当于查询 \(i,j\) 身份是否相同,因此询问 \((i,j),(j,i)\) 的答案相同时,说明这两人中没有伪装者,反之说明其中恰有一个伪装者。
如果 \(n\) 为偶数,\(n-2\) 次询问可以确定伪装者在哪两人之间,再询问两次即可确定答案。
否则需要 \(n+1\) 次,尝试优化,我们发现如果询问一个环,环上没有伪装者当且仅当有偶数个人回答否。
因此先询问一个三元环,即可变成 \(n\) 为偶数的情况,如果伪装者在三元环中,询问两条反向边即可确定答案。
此时在 \(n>3\) 时都可以做到 \(n\) 次询问。
可以证明不能更小,否则我们可以把欺骗者安排到没有入度的点或没有出度的点上。
时间复杂度 \(\mathcal O(n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
bool qry(int x,int y) {
cout<<"? "<<x<<" "<<y<<endl;
int z; cin>>z; return z^1;
}
void solve() {
int n;
cin>>n;
if(n&1) {
if(n==3) {
if(qry(1,2)!=qry(2,1)) {
bool o=qry(1,3)^qry(3,1);
cout<<"! "<<(o?1:2)<<endl;
} else cout<<"! "<<3<<endl;
return ;
}
int a=qry(n,n-1),b=qry(n-1,n-2),c=qry(n-2,n);
if((a+b+c)&1) {
a^=qry(n-1,n),b^=qry(n-2,n-1);
cout<<"! "<<(a&&b?n-1:(a?n:n-2))<<endl;
return ;
} else n-=3;
}
int p=1,q=3;
for(int i=3;i<n;i+=2) if(qry(i,i+1)^qry(i+1,i)) {
p=i,q=1; break;
}
bool o=qry(p,q)^qry(q,p);
cout<<"! "<<(o?p:p+1)<<endl;
}
signed main() {
int T; cin>>T;
while(T--) solve();
return 0;
}
C. [CF2023D] Many Games
题目大意
给定 \(n\) 个物品 \((p_i,w_i)\),选出若干个元素,最大化 \(\prod p_i\sum w_i\),其中 \(p_i=1\%\sim 100\%\)。
数据范围:\(n\le 2\times 10^5,w_i\times p_i\le 2000\)。
思路分析
记 \(P=\prod p_i,S=\sum w_i\)。
设 \(p_i=p\) 的元素选择了 \(k\) 个,那么删去最小值后的权值不优于当前方案,故 \(p^kS> p^{k-1}\dfrac{k-1}kS\),从而 \(k<\dfrac{1}{1-p}\)。
因此可以把元素个数优化到 \(P\ln P\) 级别,其中 \(P=100\)。
从最优解中删除一个元素,不可能更优,故:\(PS>\dfrac{P}{p_i}(S-w_i)\),从而 \(S<\dfrac{w_i}{1-p_i}\),由于 \(w_ip_i\le 2000\)。
因此 \(w_i\le \dfrac{2000}{(1-p_i)p_i}\),在 \(p_i=1\%\) 时取到最小值,大约为 \(\mathcal O(VP)\) 级别,其中 \(B=2000\)。
然后按 \(S\) 暴力背包即可。
时间复杂度 \(\mathcal O(n\log n+VP^2)\)。
代码呈现
#include<bits/stdc++.h>
#define ld long double
using namespace std;
const int MAXN=2e5+5,Q=100;
vector <int> A[105];
int n,p[MAXN],w[MAXN];
ld f[MAXN];
signed main() {
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d%d",&p[i],&w[i]),A[p[i]].push_back(i);
vector <int> it;
for(int p=1;p<Q;++p) {
sort(A[p].begin(),A[p].end(),[&](int i,int j){ return w[i]>w[j]; });
int c=min((int)A[p].size(),Q/(Q-p)+1);
for(int i=0;i<c;++i) it.push_back(A[p][i]);
}
f[0]=1;
for(int i:it) for(int j=MAXN-1;j>=w[i];--j) {
f[j]=max(f[j],f[j-w[i]]*p[i]/100);
}
ld s=0,z=0;
for(int i:A[Q]) s+=w[i];
for(int j=0;j<MAXN;++j) z=max(z,(j+s)*f[j]);
printf("%.20Lf",z);
return 0;
}
D. [CF2025G] Variable Damage
题目大意
游戏里有 \(n\) 个人,每个人可以携带一件装备,假设共有 \(m\) 件装备,则每个人和装备每秒都会受到 \(\dfrac 1{n+m}\) 的伤害。
每个人和装备都有血量,血量为 \(0\) 时会消失,一个人消失的时候他的装备也会消失。
\(q\) 次操作,每次加入一个装备或人,动态维护游戏至多能进行多久。
数据范围:\(q\le 3\times 10^5\)。
思路分析
我们只要统计每个东西能承受多少伤害,一个血量为 \(x\) 的人显然能受 \(x\) 点伤害,而他的装备血量为 \(y\),则能承受 \(\min(x,y)\) 点伤害。
那么我们要求的就是最大匹配,先假设每个装备的贡献就是 \(y\),然后要求有多少贡献实际达不到,相当于对于每个 \(k\),如果设血量 \(\ge k\) 的人数为 \(a_k\),装备数为 \(b_k\),那么要减去的贡献就是 \(\sum \max(0,b_k-a_k)\)。
因此我们就把这个问题转化为了数据结构问题:前缀加 \(\pm 1\),查询全局正数和。
感觉上 \(\mathrm{polylog}\) 做法是困难的,因此考虑分块,对于散块修改可以直接重构,整块时打标记。
查询的时候,设当前块标记为 \(x\),我们要求的就是 \(\sum\limits_{b_k-a_k\ge -x}b_k-a_k+x\),重构时对 \(b_k-a_k\) 开桶,由于每次 \(x\) 的变化只有 \(\pm1\),因此动态维护后缀和是简单的。
时间复杂度 \(\mathcal O(q\sqrt q)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5;
int n,m,a[MAXN],op[MAXN],st[MAXN],w[MAXN];
ll ans[MAXN],f[MAXN<<1],g[MAXN<<1];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>m;
for(int i=1;i<=m;++i) cin>>op[i]>>a[i],st[++n]=a[i];
sort(st+1,st+n+1),n=unique(st+1,st+n+1)-st-1;
for(int i=1;i<=m;++i) a[i]=lower_bound(st+1,st+n+1,a[i])-st;
for(int i=1;i<=m;++i) ans[i]=ans[i-1]+st[a[i]];
for(int l=1,r,B=sqrt(n);l<=n;l=r+1) {
r=min(n,l+B-1);
memset(f,0,sizeof(f));
memset(g,0,sizeof(g));
for(int j=l;j<=r;++j) {
f[m-w[j]]+=st[j]-st[j-1];
g[m-w[j]]+=1ll*(st[j]-st[j-1])*w[j];
}
ll cnt=f[m],sum=g[m];
for(int i=1,tg=0;i<=m;++i) {
int x=a[i],c=(op[i]==1?-1:1);
if(x>=r) {
if(c==1) ++tg,cnt+=f[m+tg],sum+=g[m+tg];
else cnt-=f[m+tg],sum-=g[m+tg],--tg;
} else if(x>=l) {
cnt=0,sum=0;
for(int j=l;j<=r;++j) f[m-w[j]]=g[m-w[j]]=0;
for(int j=l;j<=r;++j) {
w[j]+=tg+(j<=x?c:0);
if(w[j]>=0) {
cnt+=st[j]-st[j-1];
sum+=1ll*(st[j]-st[j-1])*w[j];
}
f[m-w[j]]+=st[j]-st[j-1];
g[m-w[j]]+=1ll*(st[j]-st[j-1])*w[j];
}
tg=0;
}
ans[i]-=sum+tg*cnt;
}
}
for(int i=1;i<=m;++i) cout<<ans[i]<<"\n";
return 0;
}
*E. [CF2023E] Tree of Life
题目大意
给定 \(n\) 棵点的树,选出最少的简单路径,使得每对相邻的边都至少在一条路径中相邻出现过。
数据范围:\(n\le 5\times 10^5\)。
思路分析
先选出所有长度为 \(2\) 的路径,然后可以合并端点相同的一些。
对于每个子树 \(u\),我们求出子树内的结果后,应该在保证子树内答案不变的情况下,向上的路径尽可能多,记为 \(f_u\)。
那么我们的思路就是,对于 \(u\) 的每个儿子 \(v\),我们可以把 \(\min(f_v,\mathrm{deg}_u-1)\) 过 \(v\to u\) 的路径合并起来。
如果 \(f_v>\mathrm{deg}_u-1\),那么剩余的所有路径提出来,传递到 \(f_u\) 中即可。
但是此时我们发现这个做法不一定最优,因为当我们处理完 \(f_1\) 后,多余的路径全部浪费了,因此对于有一些路径,在 \(u\) 处合并可能更优。
但我们无法在 \(u\) 处确定祖先需要多少条链,因此我们先在 \(u\) 处合并,然后在祖先的有需要的时候拆开若干条,使得 \(f_v\ge \mathrm{deg}_u-1\)。
很显然在 \(u\) 处合并后,还能随意在祖先处拆开,因此我们只要最大化子树中这样合并的路径个数 \(g_u\)。
我们已经知道每个子树还有多少条剩余的路径,只要两两匹配即可,当且仅当这些路径数不存在绝对众数时有解。
但如果存在绝对众数,我们依然可以通过拆掉其他子树内的 \(g_v\),获得新的两条路径,和绝对众数匹配,那么求出其他子树的 \(g_v\) 之和,拆掉最少的链,使得绝对众数不存在即可。
时间复杂度 \(\mathcal O(n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e5+5;
vector <int> G[MAXN];
ll f[MAXN],g[MAXN],ans;
void dfs(int u,int fz) {
int s=G[u].size()-1;
ans+=1ll*s*(s+1)/2;
array <ll,2> mx={0,0};
ll p=0;
for(int v:G[u]) if(v^fz) {
dfs(v,u);
if(f[v]<s) {
ll z=min(g[v],(s-f[v]+1)/2);
g[v]-=z,f[v]+=2*z;
}
if(f[v]>s) {
ans-=s,f[v]-=s,p+=f[v];
mx=max(mx,{f[v],g[v]});
} else ans-=f[v];
g[u]+=g[v];
}
if(p<mx[0]*2) {
ll z=min(g[u]-mx[1],(mx[0]*2-p+1)/2);
g[u]-=z,p+=2*z;
}
ll z=min(p/2,p-mx[0]);
f[u]=s+p-2*z,g[u]+=z;
}
void solve() {
int n;
scanf("%d",&n),ans=0;
for(int i=1;i<=n;++i) f[i]=g[i]=0,G[i].clear();
for(int i=1,u,v;i<n;++i) scanf("%d%d",&u,&v),G[u].push_back(v),G[v].push_back(u);
dfs(1,0),ans-=g[1],printf("%lld\n",ans);
}
signed main() {
int T; scanf("%d",&T);
while(T--) solve();
return 0;
}
*F. [CF2023F] Hills and Pits
题目大意
给定 \(n\) 个沙坑,高度为 \(a_1\sim a_n\),有正有负。
你可以在 \([1,n]\) 数轴上左右游走,你手中初始没有沙子,经过 \(a_i>0\) 的沙坑可以取走 \([0,a_i]\) 个沙子,经过 \(a_i<0\) 的沙坑可以放入 \([0,-a_i]\) 个沙子,要求每个沙坑上恰好有 \(0\) 个沙子,求最小步数。
\(q\) 次询问,对 \(a_l\sim a_r\) 求答案。
数据范围:\(n,q\le 3\times 10^5\)。
思路分析
首先分析答案下界,如果 \(\sum a_i<0\) 显然不可能,否则答案肯定不超过 \(2n\),因为我们可以从左往右取走所有正数,然后从右往左填平所有负数。
如果想要答案更小,我们就要避免每个位置都经过两次,不妨假设路径是从 \(s\to t\) 且 \(s<t\),由于 \([l,s)\) 与 \((t,r]\) 都已经走过了,那么我们的目标就是不在 \([s,t]\) 中走回头路。
此时我们对路径是 \(s\to l\to r\to t\),因此一个 \([s,t]\) 中的 \(a_i<0\) 的位置,如果 \(\sum _{j=l}^i a_j\ge 0\) 时,我们能直接填完 \(a_i\),否则需要找到 \(>i\) 的第一个前缀和 \(\ge 0\) 的位置,然后把这一段负的前缀和全部填成正的。
因此此时的权值是 \(\sum_{i=s}^{t-1}[v_i\ge0]-[v_i<0]\),其中 \(v_i\) 表示 \(a[l,i]\) 的前缀和。
所以答案就是最大子段和,但 \(v\) 的计算和 \(l\) 有关,故不能直接线段树维护。
设 \(S_i\) 为全局前缀和,注意到 \(v_i\ge 0\iff S_i\ge S_{l-1}\),因此我们把询问按 \(S_{l-1}\) 从小到大排序后扫描线,就只需要 \(\mathcal O(n)\) 次修改,线段树维护最大子段和即可。
\(t>s\) 的情况是对称的,翻转 \(a\) 重复上述过程。
时间复杂度 \(\mathcal O((n+q)\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5;
struct info {
int su,mx,lx,rx;
inline friend info operator +(const info &u,const info &v) {
return {u.su+v.su,max({u.mx,v.mx,u.rx+v.lx}),max(u.lx,u.su+v.lx),max(v.rx,v.su+u.rx)};
}
};
struct zKyGt1 {
info tr[1<<20];
int N;
void init(int n) {
for(N=1;N<=n;N<<=1);
for(int i=1;i<2*N;++i) tr[i]={0,0,0,0};
for(int i=1;i<=n;++i) tr[i+N]={-1,0,-1,-1};
for(int i=N-1;i;--i) tr[i]=tr[i<<1]+tr[i<<1|1];
}
void upd(int x) {
for(tr[x+=N]={1,1,1,1},x>>=1;x;x>>=1) tr[x]=tr[x<<1]+tr[x<<1|1];
}
int qry(int l,int r) {
info sl={0,0,0,0},sr={0,0,0,0};
for(l+=N-1,r+=N+1;l^r^1;l>>=1,r>>=1) {
if(~l&1) sl=sl+tr[l^1];
if(r&1) sr=tr[r^1]+sr;
}
return (sl+sr).mx;
}
} T;
ll a[MAXN],s[MAXN];
int n,m,l[MAXN],r[MAXN],ans[MAXN];
vector <int> qy[MAXN];
void sol() {
T.init(n);
vector <int> id;
for(int i=1;i<=n;++i) qy[i].clear(),s[i]=s[i-1]+a[i];
for(int i=1;i<=m;++i) if(l[i]<r[i]) qy[l[i]].push_back(i);
for(int i=0;i<=n;++i) id.push_back(i);
sort(id.begin(),id.end(),[&](int i,int j){ return s[i]^s[j]?s[i]>s[j]:i>j; });
for(int i:id) {
if(i>0) T.upd(i);
for(auto q:qy[i+1]) {
ans[q]=min(ans[q],2*(r[q]-l[q])-T.qry(l[q],r[q]-1));
}
}
}
void solve() {
cin>>n>>m;
for(int i=1;i<=n;++i) cin>>a[i];
for(int i=1;i<=m;++i) cin>>l[i]>>r[i],ans[i]=2*(r[i]-l[i]);
sol();
reverse(a+1,a+n+1);
for(int i=1;i<=m;++i) swap(l[i],r[i]),l[i]=n-l[i]+1,r[i]=n-r[i]+1;
sol();
for(int i=1;i<=m;++i) cout<<(s[r[i]]<s[l[i]-1]?-1:ans[i])<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}

浙公网安备 33010602011771号