2025 省选做题记录(四)
\(\text{By DaiRuichen007}\)
Round #45 - 20250202
*A. 会议室(meeting)
题目大意
给定 \(n\) 个区间,依次删除每个区间,最小化每次删除后剩余区间构成的连通块个数之和,求方案数。
数据范围:\(n\le 2000\)。
思路分析
首先考虑最小代价,很显然我们会从小到大依次删除每个连通块,且删除保持该连通块不分裂,这是简单的,每次删掉最右边的线段即可做到。
那么我们求出每个连通块内部的删除序列个数乘起来,再对于大小相同的序列任意排列即可。
求合法删除序列难以刻画,不妨倒过来求合法插入序列,即每次插入线段都和原本线段有交,那么状态就是 \(f_{l,r,i}\) 表示插入 \(i\) 条线段,并为 \([l,r]\) 的方案数。
暴力枚举可以做到四次方转移。
考虑优化掉 \(i\) 一维状态,只考虑 \([l,r]\) 扩展的时刻,设这些线段是 \(s_1\sim s_k\),并且插入 \(s_i\) 后,被新的线段并包含的集合为 \(T_i\)。
那么我们要数这 \(n\) 条线段的拓扑序,我们的限制就是 \(s_i\) 要比 \(s_{i+1}\) 和 \(A_i\) 先插入。
那么该结构构成一棵外向树,拓扑序就是 \(\dfrac{n!}{\prod_{i=1}^n\sum_{j\ge i} 1+|A_j|}\)。
那么 dp 的时候只要记录 \(f_{l,r}\) 新加一条线段,其子树大小就是 \(n-s_{l,r}\),其中 \(s_{l,r}\) 是子树内线段个数。
转移时枚举新加入的线段,然后前缀和优化 dp 即可。
时间复杂度 \(\mathcal O(n^2)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=4005,MOD=1e9+7;
struct seg { int l,r; };
int L[MAXN],R[MAXN],cnt[MAXN],p[MAXN],c[MAXN][MAXN];
ll fac[MAXN],inv[MAXN],f[MAXN][MAXN],s[MAXN][MAXN],sl[MAXN][MAXN],sr[MAXN][MAXN];
ll solve(vector<seg>A) {
int n=A.size(); ++cnt[n];
vector <int> vl;
for(auto o:A) vl.push_back(o.l),vl.push_back(o.r);
sort(vl.begin(),vl.end());
for(int i=0;i<n;++i) {
L[i+1]=lower_bound(vl.begin(),vl.end(),A[i].l)-vl.begin()+1;
R[i+1]=lower_bound(vl.begin(),vl.end(),A[i].r)-vl.begin()+1;
}
for(int i=1;i<=2*n;++i) {
memset(f[i],0,sizeof(f[i]));
memset(c[i],0,sizeof(c[i]));
memset(s[i],0,sizeof(s[i]));
memset(sl[i],0,sizeof(sl[i]));
memset(sr[i],0,sizeof(sr[i]));
}
for(int i=1;i<=n;++i) p[L[i]]=R[i],p[R[i]]=L[i],++f[L[i]][R[i]],++c[L[i]][R[i]];
for(int l=2*n;l>=1;--l) for(int r=l;r<=2*n;++r) c[l][r]+=c[l+1][r]+c[l][r-1]-c[l+1][r-1];
for(int l=2*n;l>=1;--l) for(int r=l;r<=2*n;++r) {
if(p[l]==r) f[l][r]+=s[l+1][r-1];
if(l<p[l]&&p[l]<r) f[l][r]+=sr[l+1][r]-sr[p[l]][r];
if(l<p[r]&&p[r]<r) f[l][r]+=sl[l][r-1]-sl[l][p[r]];
f[l][r]=(f[l][r]%MOD+MOD)%MOD;
ll z=f[l][r]*inv[n-c[l][r]]%MOD;
s[l][r]=(z+s[l][r-1]+s[l+1][r]+MOD-s[l+1][r-1])%MOD;
sl[l][r]=(sl[l][r-1]+z)%MOD,sr[l][r]=(sr[l+1][r]+z)%MOD;
}
return f[1][2*n]*fac[n-1]%MOD;
}
int count_removals(vector<int>S,vector<int>E) {
for(int i=fac[0]=1;i<MAXN;++i) fac[i]=fac[i-1]*i%MOD;
inv[0]=inv[1]=1;
for(int i=2;i<MAXN;++i) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
vector <seg> A;
for(int i=0;i<(int)S.size();++i) A.push_back({S[i],E[i]});
sort(A.begin(),A.end(),[&](auto i,auto j){ return i.l<j.l; });
vector <seg> B;
ll ans=1;
int lst=0;
for(auto o:A) {
if(B.empty()||o.l<lst) lst=max(lst,o.r),B.push_back(o);
else ans=ans*solve(B)%MOD,B={o},lst=o.r;
}
ans=ans*solve(B)%MOD;
for(int i=1;i<=(int)A.size();++i) ans=ans*fac[cnt[i]]%MOD;
return ans;
}
B. 学生(student)
题目大意
给定 \(n\) 个人,以及 \(m\) 个集合,第 \(i\) 个集合为 \([1,l_i-1]\cup[r_i+1,n]\),每个人只知道自己在哪些集合中,以及集合个数 \(\ge 1\)。
如果一个人推断出存在不包含自己的集合,他就会离开,求最早的离开时刻,以及该时刻离开了几个人。
数据范围:\(n,m\le 2\times 10^5\)。
思路分析
经典问题,设 \(f_s\) 表示仅考虑 \(s\) 中线段,第一个人离开的时刻,那么转移的时候考虑一个人 \(u\) 的推理,\(u\) 假设所有集合都和 \(u\) 有交,设包含 \(u\) 的集合为 \(A_u\),那么 \(u\) 会在 \(f_{s\cap A_u}+1\) 时刻离开。
那么 \(f_s=1+\min_u f_{s\cap A_u}\),注意到 \(A_u\) 就是所有不包含 \(u\) 的区间,因此 \(f_s\) 实际上的组合意义就是选出最少的点覆盖所有 \([l,r]\)。
求 \(f_{U}\) 直接朴素贪心即可。
检验 \(u\) 是否是第一个离开的,只需要考虑 \([1,u-1]\) 和 \([u+1,n]\) 内部线段的答案之和是否等于 \(f_U-1\)。
而前缀后缀内部的答案也是简单的,分别按左右端点排序后贪心即可。
时间复杂度 \(\mathcal O(n+m\log m)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2.5e5+5;
struct seg { int l,r; } a[MAXN];
int f[MAXN],g[MAXN];
pair<int,vector<int>> complaint(int n,vector<int>L,vector<int>R) {
int m=0;
for(int i=0;i<(int)L.size();++i) a[++m]={L[i]+1,R[i]+1};
sort(a+1,a+m+1,[&](auto i,auto j){ return i.r<j.r; });
for(int i=1,lst=0,cnt=0;i<=m;++i) {
if(lst<a[i].l) lst=a[i].r,++cnt;
f[a[i].r]=cnt;
}
sort(a+1,a+m+1,[&](auto i,auto j){ return i.l>j.l; });
for(int i=1,lst=n+1,cnt=0;i<=m;++i) {
if(a[i].r<lst) lst=a[i].l,++cnt;
g[a[i].l]=cnt;
}
for(int i=1;i<=n;++i) f[i]=max(f[i],f[i-1]);
for(int i=n;i>=1;--i) g[i]=max(g[i],g[i+1]);
int ans=f[n];
vector <int> ret;
for(int i=1;i<=n;++i) if(f[i-1]+g[i+1]==ans-1) ret.push_back(i-1);
return {ans,ret};
}
C. 团队建设(team)
题目大意
给定 \(\{a_n\},\{b_n\},\{c_m\},\{d_m\}\),其中 \(a,c\) 递增,\(b,d\) 递减,\(q\) 次询问 \(\max_{l_1\le i\le r_1}\max_{l_2\le j\le r_2}(a_i+c_j)(b_i+d_j)\)。
数据范围:\(n,m,q\le 10^5\)。
思路分析
首先考虑全局询问,容易发现对于每个 \(i\),最优的 \(j\) 具有决策单调性,即 \(a_i\) 越大匹配的 \(c_j\) 越小。
那么我们可以考虑对序列 \(c,d\) 分块,设块长为 \(B\)。
对于 \([l_2,r_2]\) 内部的每个整块,我们离线逐块处理,用单调队列优化决策单调性 dp,可以做到 \(\mathcal O(n+B\log n)\) 的复杂度求出每个 \(i\) 在当前块内匹配 \(j\) 得到的最大权值,然后静态 RMQ 即可。
然后考虑散块,可以看作 \(\mathcal O(qB)\) 个 \(l_2=r_2\) 的询问,此时对 \([l_1,r_1]\) CDQ 分治。
如果 \(l_1\le mid<r_1\),那么分成 \([l_1,mid],(mid,r_1]\) 两个部分,分别扫描线,动态维护单调队列优化 dp,查询的时候直接在单调队列上二分 \(l_2\) 所属的决策即可。
注意到 \(\mathcal O(qB)\) 个询问实际上是 \(\mathcal O(q)\) 个长度和 \(\mathcal O(qB)\) 的区间,那么对于每个区间二分 \(l_2\) 所属的决策,\(l_2+1\sim r_2\) 所属的决策可以直接算出。
时间复杂度 \(\mathcal O(n\log^2n+qB)\)。
假设 \(n,m,q\) 同阶,瓶颈在于整块处理处的静态 RMQ,代码中采用 zkw 线段树。
时间复杂度 \(\mathcal O(n\log^2n+n\sqrt {n\log n})\)。
如果使用 \(\mathcal O(n)-\mathcal O(1)\) RMQ 可以做到 \(\mathcal O(n\log^2n+n\sqrt n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+5,B=500;
int n,m,q;
ll a1[MAXN],b1[MAXN],a2[MAXN],b2[MAXN],ans[MAXN];
int bl[MAXN],lp[MAXN],rp[MAXN];
ll w(int i,int j) { return (a1[i]+a2[j])*(b1[i]+b2[j]); }
int l1[MAXN],r1[MAXN],l2[MAXN],r2[MAXN];
struct info { int l,r,x; } st[MAXN];
vector <int> qy[MAXN];
void cdq(int l,int r,vector<int>&qid) {
if(l==r) {
for(int o:qid) {
int ql=l2[o],qr=r2[o];
if(bl[ql]==bl[qr]) for(int j=l2[o];j<=r2[o];++j) ans[o]=max(ans[o],w(l,j));
else {
for(int j=ql;j<=rp[bl[ql]];++j) ans[o]=max(ans[o],w(l,j));
for(int j=lp[bl[qr]];j<=qr;++j) ans[o]=max(ans[o],w(l,j));
}
}
return ;
}
int mid=(l+r)>>1;
vector <int> QL,QR;
for(int i=l;i<=r;++i) qy[i].clear();
for(int o:qid) {
if(r1[o]<=mid) QL.push_back(o);
else if(mid<l1[o]) QR.push_back(o);
else qy[l1[o]].push_back(o),qy[r1[o]].push_back(o);
}
for(int i=mid,tp=0;i>=l;--i) {
while(tp&&w(st[tp].x,st[tp].l)<=w(i,st[tp].l)) --tp;
if(!tp) st[++tp]={1,m,i};
else {
int ul=st[tp].l,ur=st[tp].r,p=ul;
while(ul<=ur) {
int um=(ul+ur)>>1;
if(w(st[tp].x,um)>w(i,um)) p=um,ul=um+1;
else ur=um-1;
}
st[tp].r=p;
if(p<m) st[++tp]={p+1,m,i};
}
auto qry=[&](int o,int L,int R) {
int ul=1,ur=tp,p=1;
while(ul<=ur) {
int um=(ul+ur)>>1;
if(st[um].l<=L) p=um,ul=um+1;
else ur=um-1;
}
for(int j=L;j<=R;++j) {
while(st[p].r<j) ++p;
ans[o]=max(ans[o],w(st[p].x,j));
}
};
for(auto o:qy[i]) {
int ql=l2[o],qr=r2[o];
if(bl[ql]==bl[qr]) qry(o,ql,qr);
else qry(o,ql,rp[bl[ql]]),qry(o,lp[bl[qr]],qr);
}
}
for(int i=mid+1,tp=0;i<=r;++i) {
while(tp&&w(st[tp].x,st[tp].r)<=w(i,st[tp].r)) --tp;
if(!tp) st[++tp]={1,m,i};
else {
int ul=st[tp].l,ur=st[tp].r,p=ur;
while(ul<=ur) {
int um=(ul+ur)>>1;
if(w(st[tp].x,um)>w(i,um)) p=um,ur=um-1;
else ul=um+1;
}
st[tp].l=p;
if(p>1) st[++tp]={1,p-1,i};
}
auto qry=[&](int o,int L,int R) {
int ul=1,ur=tp,p=tp;
while(ul<=ur) {
int um=(ul+ur)>>1;
if(R<=st[um].r) p=um,ul=um+1;
else ur=um-1;
}
for(int j=R;j>=L;--j) {
while(j<st[p].l) ++p;
ans[o]=max(ans[o],w(st[p].x,j));
}
};
for(auto o:qy[i]) {
int ql=l2[o],qr=r2[o];
if(bl[ql]==bl[qr]) qry(o,ql,qr);
else qry(o,ql,rp[bl[ql]]),qry(o,lp[bl[qr]],qr);
}
}
cdq(l,mid,QL),cdq(mid+1,r,QR);
}
const int N=1<<17;
ll tr[N<<1];
vector <ll> build_teams(vector<int>A1,vector<int>B1,vector<int>A2,vector<int>B2,
vector<int>L1,vector<int>R1,vector<int>L2,vector<int>R2) {
n=A1.size(),m=A2.size(),q=L1.size();
for(int i=1;i<=n;++i) a1[i]=A1[i-1],b1[i]=B1[i-1];
for(int i=1;i<=m;++i) a2[i]=A2[i-1],b2[i]=B2[i-1];
for(int i=1;(i-1)*B+1<=m;++i) {
lp[i]=(i-1)*B+1,rp[i]=min(i*B,m);
fill(bl+lp[i],bl+rp[i]+1,i);
}
vector <int> qid;
for(int i=1;i<=q;++i) l1[i]=L1[i-1]+1,r1[i]=R1[i-1]+1,l2[i]=L2[i-1]+1,r2[i]=R2[i-1]+1,qid.push_back(i);
cdq(1,n,qid);
for(int x=1;x<=bl[m];++x) {
int tp=0;
for(int i=rp[x];i>=lp[x];--i) {
while(tp&&w(st[tp].l,st[tp].x)<=w(st[tp].l,i)) --tp;
if(!tp) st[++tp]={1,n,i};
else {
int ul=st[tp].l,ur=st[tp].r,p=ul;
while(ul<=ur) {
int um=(ul+ur)>>1;
if(w(um,st[tp].x)>w(um,i)) p=um,ul=um+1;
else ur=um-1;
}
st[tp].r=p;
if(p<n) st[++tp]={p+1,n,i};
}
}
memset(tr,0,sizeof(tr));
for(int i=1;i<=tp;++i) for(int j=st[i].l;j<=st[i].r;++j) {
tr[j+N]=w(j,st[i].x);
}
for(int i=N-1;i;--i) tr[i]=max(tr[i<<1],tr[i<<1|1]);
for(int i=1;i<=q;++i) if(bl[l2[i]]<x&&x<bl[r2[i]]) {
for(int ql=l1[i]+N-1,qr=r1[i]+N+1;ql^qr^1;ql>>=1,qr>>=1) {
if(~ql&1) ans[i]=max(ans[i],tr[ql^1]);
if(qr&1) ans[i]=max(ans[i],tr[qr^1]);
}
}
}
return vector<ll>(ans+1,ans+q+1);
}
Round #46 - 20250203
A. 置换(perm)
题目大意
给定 \(n\) 阶排列 \(p\),求有多少排列 \(q^k=p\)。
数据范围:\(n\le 3000\)。
思路分析
\(q\) 中一个大小为 \(s\) 的环会变成 \(p\) 中 \(c=\gcd(k,s)\) 个等大的环。
那么对于 \(p\) 中 \(c\) 个大小为 \(s\) 的环,可以以 \(s^{c-1}(c-1)!\) 的系数合并,注意还要除以 \(c\) 的多重集排列以及 \(\prod c!\)。
因此 \(f_i\) 表示处理 \(i\) 个环的方案数,对于每个 \(c\) 一次性枚举选了几个环。
时间复杂度 \(\mathcal O(n^2\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3005,MOD=998244353;
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; }
vector <int> c[MAXN];
int n,k,a[MAXN],p[MAXN];
bool vis[MAXN];
ll fac[MAXN],ifac[MAXN],f[MAXN],g[MAXN];
void solve() {
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i) scanf("%d",&p[i]),a[i]=vis[i]=0,c[i].clear();
for(int i=1;i<=n;++i) if(!vis[i]) {
int s=0;
for(int u=i;!vis[u];u=p[u]) vis[u]=true,++s;
++a[s];
}
for(int i=1;i<=n;++i) {
int r=__gcd(i,k);
c[i/r].push_back(r);
}
ll ans=1;
for(int i=1;i<=n;++i) if(a[i]) {
memset(f,0,sizeof(f)),f[0]=1;
for(int s:c[i]) {
memset(g,0,sizeof(g));
ll vl=ifac[s]*fac[s-1]%MOD*ksm(i,s-1)%MOD,pw=1;
for(int j=0;s*j<=a[i];++j,pw=pw*vl%MOD) for(int x=s*j;x<=a[i];++x) {
g[x]=(g[x]+f[x-s*j]*pw%MOD*ifac[j])%MOD;
}
memcpy(f,g,sizeof(f));
}
ans=ans*f[a[i]]%MOD*fac[a[i]]%MOD;
}
printf("%lld\n",ans);
}
signed main() {
for(int i=fac[0]=ifac[0]=1;i<MAXN;++i) ifac[i]=ksm(fac[i]=fac[i-1]*i%MOD);
int _; scanf("%d",&_);
while(_--) solve();
return 0;
}
*B. 黑洞(interact)
题目大意
交互器中有一个 \(x\in[1,n]\),每次询问 \(k\) 可以得到 \([x\ge k]\),但交互器会撒至多一次谎,构造最优策略。
数据范围:\(n\le 3\times 10^4\)。
思路分析
首先考虑如何维护答案,求出每个 \(x\) 违反了几个询问 \(a_x\),那么一次询问相当于给 \(a[1,x-1]/a[x,n]\) 全体加一。
我们的目标就是让 \(a_x\le 1\) 的元素唯一。
很显然 \(a_x\ge 2\) 的元素此后完全不会被考虑,那么我们只要取出 \(a_x\in\{0,1\}\) 的子序列,容易发现这个子序列的形式一定是 \(111000111\)。
那么设 \(dp_{i,j,k}\) 表示当前 \(a\) 为 \(i\) 个 \(1\),\(j\) 个 \(0\),\(k\) 个 \(1\) 拼接时的答案,转移时直接枚举询问 \(p\) 在哪个地方,复杂度 \(\mathcal O(n^4)\)。
注意到 dp 值域很小,因此 \(f_{c,i,j}\) 表示 \(\max\{k\mid dp_{i,j,k}\le c\}\),只需要分讨 \(p\) 属于哪个段,可以直接做到 \(\mathcal O(1)\) 转移。
时间复杂度 \(\mathcal O(n^2\log n)\),可以处理 \(n\le 5000\) 的情况。
然后直接暴力打表记决策点,注意到本地静态空间无法超过 2G,因此用 std::vector 在代码里申请就能开得下。
我们并不需要记录所有决策点,首先 \(i+j+k\le 5000\) 的部分可以预处理。
并且已知 \(17\) 次操作最多处理 \(n=6444\),那么如果 \(n<6444\),那么直接补成 \(n=6444\) 也能在 \(17\) 次操作内求出答案。
那么我们只要打表出 \(n=6444,12286,23464,30000\) 处的所有决策点即可,Code Link。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5000,M=17,MAXN=3e4+5,inf=1e9;
inline void chkmin(int &x,const int &y) { x=y<x?y:x; }
inline void chkmax(short &x,const short &y) { x=y>x?y:x; }
short f[M+1][N+5][N+5]; int lg[MAXN+5],up[M+1];
array <int,4> str[]={ /*all possible transition pos*/ };
void init(int n) {
for(int c=0;c<=M;++c) up[c]=min(1<<c,n);
memset(f,-0x3f,sizeof(f));
f[0][0][0]=1,f[0][0][1]=f[0][1][0]=0;
for(int c=0;c<=M;++c) {
for(int i=up[c];i>=0;--i) for(int j=up[c]-i;j>=0;--j) {
chkmax(f[c][i][j],f[c][i+1][j]),chkmax(f[c][i][j],f[c][i][j+1]);
}
if(c==M) return ;
for(int i=0;i<=up[c];++i) for(int j=0;i+j<=up[c];++j) if(f[c][i][j]>=0) {
if(f[c][j][i]>=0) chkmax(f[c+1][min(up[c+1]-i-j,(int)f[c][j][i])][i+j],f[c][i][j]); //split j
if(j<=(1<<c)) {
chkmax(f[c+1][min(up[c+1]-j,(1<<c)-j+i)][j],f[c][i][j]); //split i
chkmax(f[c+1][i][j],f[c][i][j]+(1<<c)-j); //split k
}
}
}
}
int dp(int i,int j,int k) {
if(i<=N&&j<=N) for(int c=0;c<=M;++c) if(f[c][i][j]>=k) return c;
return M+1;
}
int trs(int i,int j,int k) {
if(!j) return (i+k)/2;
if(i+j+k>N) {
for(auto it:str) if(it[0]==i&&it[1]==j&&it[2]==k) return it[3];
return -1;
}
int vl=dp(i,j,k)-1;
for(int p=1;p<=i;++p) if(vl==max(dp(i-p,j,k),lg[p+j])) return p;
for(int p=1;p<j;++p) if(vl==max(dp(p,j-p,k),dp(i,p,j-p))) return i+p;
for(int p=0;p<k;++p) if(vl==max(lg[j+k-p],dp(i,j,p))) return i+j+p;
return -1;
}
int o,st[MAXN],m,a[MAXN];
bool qry(int x) {
if(x>o) return 0;
cout<<"? "<<x<<endl; string _; cin>>_; return _=="Yes";
}
void solve(int n) {
m=0;
for(int i=1;i<=n;++i) st[++m]=i,a[i]=0;
while(m>1) {
int l=m,r=0;
for(int i=1;i<=m;++i) if(a[st[i]]==0) l=min(l,i),r=max(r,i);
int p=(l>r?m/2:trs(l-1,r-l+1,m-r))+1;
bool fl=qry(st[p]);
if(fl) for(int i=1;i<p;++i) ++a[st[i]];
else for(int i=p;i<=m;++i) ++a[st[i]];
int k=0;
for(int i=1;i<=m;++i) if(a[st[i]]<=1) st[++k]=st[i];
m=k;
}
cout<<"! "<<st[m]<<endl;
}
const int LIM[]={30000,23464,12286,6444};
signed main() {
for(int i=1;i<MAXN;++i) lg[i]=__lg(i-1)+1;
cin>>o;
init(min(N,o));
int n=3e4;
for(int p:LIM) if(o<=p) n=p;
if(o<=N) n=o;
while(true) {
solve(n);
string _; cin>>_; if(_=="Done") break;
}
return 0;
}
C. 挑战(challenge)
题目大意
给定 \(n\) 个盒子,容量 \(a_1\sim a_n\),以及 \(m\) 个机器人,第 \(i\) 个机器人可以向 \(a_{l_i}\sim a_{r_i}\) 投放总计 \(\le c_i\) 个球。
对于每个 \(x\in [1,n]\),我们把 \(t_i=1\) 的机器人变为 \((\min(l_i,x),\max(r_i,x),c_i)\),求出总共最多能放几个球。
数据范围:\(n,m\le 2\times 10^5\).
思路分析
首先这是一个最大匹配问题,直接 Ex-Hall 定理,我们求出 \(\max |A|-|N(A)|\),假设 \(A\) 是若干线段,则 \(|A|\) 是其 \(c\) 之和,而 \(N(A)\) 是被其覆盖的所有 \(a\) 之和。
定义 \(w_{l,r}\) 表示 \([l,r]\) 内部所有线段的 \(c\) 之和,减去 \(a_l+\cdots+a_r\)。
则最大匹配等于 \(\sum c_i\) 减去 \(\max\sum w_{L_i,R_i}\),其中 \(\{L_i,R_i\}\) 是若干不相交区间集合。
首先如果所有 \(t_i=1\),那么所有线段过 \(x\),我们肯定会选取一个包含 \(x\) 的区间,且其他区间权值肯定为 \(0\)。
那么我们只要求出 \(\max\{w_{l,r}\mid l\le x\le r\}\),可以扫描线维护 \(w\),因此用区间加区间历史最大值的线段树维护即可。
然后回到一般情况,首先如果不选 \(x\),那么等价于保留 \(t_i=0\) 的线段后的答案,否则假设选择 \(x\in[l,r]\),则 \(w_{l,r}\) 依然不变。
然后在 \(a[1,l-1],a[r+1,n]\) 内部也选择若干区间使得 \(\sum w\) 尽可能大,注意这里的 \(w\) 只能考虑 \(t_i=0\) 的区间,预处理一个前缀和后缀的 dp 值,这也容易用扫描线优化。
然后把答案改成 \(\max\{f_{l-1}+w_{l,r}+g_{r+1}\mid l\le x\le r\}\),依然可以历史最值线段树维护。
时间复杂度 \(\mathcal O((n+m)\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,m;
struct SegmentTree {
ll mx[MAXN<<2],tg[MAXN<<2],hmx[MAXN<<2],htg[MAXN<<2];
void init() {
memset(mx,0,n<<5),memset(tg,0,n<<5),memset(hmx,0,n<<5),memset(htg,0,n<<5);
}
void adt(int p,ll t,ll ht) {
hmx[p]=max(hmx[p],mx[p]+ht),htg[p]=max(htg[p],tg[p]+ht),mx[p]+=t,tg[p]+=t;
}
void psd(int p) {
adt(p<<1,tg[p],htg[p]),adt(p<<1|1,tg[p],htg[p]),tg[p]=htg[p]=0;
}
void psu(int p) {
mx[p]=max(mx[p<<1],mx[p<<1|1]),hmx[p]=max(hmx[p<<1],hmx[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,max(k,0ll));
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 hmx[p];
int mid=(l+r)>>1; ll s=0; psd(p);
if(ul<=mid) s=max(s,qry(ul,ur,l,mid,p<<1));
if(mid<ur) s=max(s,qry(ul,ur,mid+1,r,p<<1|1));
return s;
}
void dfs(int l=1,int r=n,int p=1) {
hmx[p]=max(0ll,mx[p]),htg[p]=0;
if(l==r) return ;
int mid=(l+r)>>1; psd(p);
dfs(l,mid,p<<1),dfs(mid+1,r,p<<1|1);
}
} T;
int a[MAXN],l[MAXN],r[MAXN],b[MAXN],op[MAXN];
ll dp[MAXN],S,fl[MAXN],fr[MAXN];
vector <int> sg[MAXN];
void solve() {
cin>>n>>m,T.init(),S=0,fl[0]=fr[n+1]=0;
for(int i=1;i<=n;++i) cin>>a[i];
for(int i=1;i<=m;++i) cin>>l[i]>>r[i]>>b[i]>>op[i],S+=b[i];
T.init();
for(int i=1;i<=n;++i) sg[i].clear();
for(int i=1;i<=m;++i) sg[l[i]].push_back(i);
for(int i=n;i>=1;--i) {
T.add(i,n,-a[i]),T.add(i,i,fr[i+1]);
for(int s:sg[i]) if(!op[s]) T.add(r[s],n,b[s]);
fr[i]=max(fr[i+1],T.mx[1]);
}
T.init();
for(int i=1;i<=n;++i) sg[i].clear();
for(int i=1;i<=m;++i) sg[r[i]].push_back(i);
for(int i=1;i<=n;++i) {
T.add(1,i,-a[i]),T.add(i,i,fl[i-1]);
for(int s:sg[i]) if(!op[s]) T.add(1,l[s],b[s]);
fl[i]=max(fl[i-1],T.mx[1]);
}
T.init();
for(int i=1;i<=n;++i) {
T.add(1,i,-a[i]),T.add(i,i,fl[i-1]);
for(int s:sg[i]) T.add(1,l[s],b[s]);
}
T.dfs();
for(int i=n;i>=1;--i) {
T.add(1,i,fr[i+1]),T.add(1,i,-fr[i+1]);
dp[i]=T.qry(1,i);
for(int s:sg[i]) T.add(1,l[s],-b[s]);
T.add(1,i,a[i]);
}
for(int i=1;i<=n;++i) cout<<S-max(dp[i],fl[n])<<" \n"[i==n];
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
Round #47 - 20250207
A. 领带(tie)
题目大意
给定 \(a_1\sim a_n\),删除若干不相邻元素,最小化剩下元素的最小非严格 LIS 覆盖。
数据范围:\(n\le 5\times 10^6,a_i\le 21\)。
思路分析
首先记录每个 LIS 的末尾,状态数 \(\mathcal O(V^V)\)。
首先注意到两个末尾为 \(x\) 的 LIS 能覆盖的序列,等价于末尾为 \(x,x+1\) 的两个 LIS 能覆盖的序列,只需要把其中一个序列开头的 \(x\) 全部塞到另一个序列里面。
那么此时每种状态的元素只有 \(\le 1\) 个,状态数 \(\mathcal O(2^V)\)。
可以暴力:\(dp_{i,s}\) 表示 \(a[1,i]\) 在保留 \(a_i\) 的情况下能否匹配到 \(s\) 这个状态,注意到 dp 值域 \(\{0,1\}\),因此设 \(f_s=\max\{i\mid dp_{i,s}=1\}\)。
转移的时候先对 \(f_s\) 做一个类似高维前缀最大值过程,然后枚举下一个保留的数是 \(a_{f_s+1}/a_{f_s+2}\)。
设保留了 \(a_j\),新的状态为 \(t\),那么转移到 \(f_t\) 相当于在 \(a[j,n]\) 中找到第一个 \(x\) 使得 \(a_{x+1},a_{x+2}\not\in t\)。
这个问题可以先二分,判定的时候枚举 \(a_{x+1}\) 的值,然后 ST 表维护区间中所有 \(a_i=v\) 的元素的 \(\{a_{i-1},a_{i+1}\}\) 的并集。
时间复杂度 \(\mathcal O(2^VV\log n)\),空间复杂度 \(\mathcal O(nV+n\log n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e6+5,m=21;
int n,a[MAXN],f[1<<m],st[24][MAXN],id[m][MAXN];
int to(int s,int v) {
if(s>>v&1) return s;
if(!(s&((1<<v)-1))) return -1;
return (s^(1<<(31-__builtin_clz(s&((1<<v)-1)))))|1<<v;
}
int qry(int l,int r) {
int k=__lg(r-l+1);
return st[k][l]|st[k][r-(1<<k)+1];
}
bool chk(int l,int r,int s) {
for(int i=0;i<m;++i) if(!(s>>i&1)) {
int x=id[i][l]+1,y=id[i][r-1];
int t=(x<=y?qry(x,y):0);
if(a[l]==i) t|=1<<a[l+1];
if(a[r]==i) t|=1<<a[r-1];
if((t&s)!=t) return false;
}
return true;
}
int nxt(int i,int s) {
if(i==n||chk(i,n,s)) return n;
int l=i+1,r=n-1,p=i;
while(l<=r) {
int mid=(l+r)>>1;
if(chk(i,mid,s)) p=mid,l=mid+1;
else r=mid-1;
}
return p-1;
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i],--a[i];
for(int x=0,tp=0;x<m;++x) {
id[x][0]=tp;
for(int i=1;i<=n;++i) {
if(a[i]==x) {
id[x][i]=++tp;
if(i>1) st[0][tp]|=1<<a[i-1];
if(i<n) st[0][tp]|=1<<a[i+1];
}
else id[x][i]=id[x][i-1];
}
}
for(int k=1;k<24;++k) {
int *f1=st[k],*f0=st[k-1];
for(int i=1;i+(1<<k)-1<=n;++i) {
f1[i]=f0[i]|f0[i+(1<<(k-1))];
}
}
for(int s=1;s<(1<<m);++s) f[s]=1;
for(int d=1;d<m;++d) {
for(int s=1;s<(1<<m);++s) if(__builtin_popcount(s)==d) {
f[s]=nxt(f[s],s);
for(int i=0,pr=-1;i<m;++i) {
if(s>>i&1) {
if(~pr) f[s]=max(f[s],f[s^(1<<i)^(1<<pr)]);
} else pr=i;
}
if(f[s]>=n) return cout<<d<<"\n",0;
for(int j:{f[s]+1,f[s]+2}) {
int t=to(s,a[j]);
if(~t) f[t]=max(f[t],j);
}
}
}
cout<<m<<"\n";
return 0;
}
*B. 乘积(prod)
题目大意
给定 \(a_1\sim a_n\) 初始全为 \(0\),进行 \(c\) 次操作,第 \(i\) 次操作会以 \(b_x\) 为权重,随机一个区间 \(a[x,x+n-1]\) 并区间加一,求最终 \(\prod a_i\) 的期望。
数据范围:\(n\le 50\)。
思路分析
先把 \(\prod a_i\) 拆开,变成每个点选择一个覆盖该点的操作,求选择方案数。
那么对于每个操作,我们只关心选择了这个操作的所有点中最小的和最大的点,这样就能确定这个区间的 \(x\) 的范围从而计算贡献。
假设 \(S_i\) 表示恰好有 \(i\) 个操作被至少一个点选择的方案数,那么答案为 \(\sum S_ic^{\underline i}\)。
可以 dp,设 \(f_{i,s,j}\) 表示确定 \(a[1,i]\) 的状态,当前有 \(j\) 个操作,且 \([i-k+1,i-1]\) 范围内还有一些之确定最小点,最大点 \(>i\) 的操作,这些最小点为集合 \(s\)。
转移的时候分讨 \(i+1\) 是左端点、右端点、单点、或某个被经过的点,可以做到 \(\mathcal O(n)\)。
时间复杂度 \(\mathcal O(n^32^m)\)。
注意到 \(2m>n\) 的时候,中间的 \(2m-n\) 个数每次操作都会被覆盖,那么此时就能转成 \(2m\le n\) 的情况,时间复杂度 \(\mathcal O(n^32^{n/2})\)。
但是这样依然无法通过,我们只能处理 \(m\le 16\) 的情况。
剩余的情况一定有 \(3m>n\),不妨加入一些 \(b=0\) 的区间使得 \(n=3m\)。
此时不妨直接观察哪些 \(a\) 是合法的。
首先对 \(a[1,m],a[2m+1,3m]\) 差分,能得到每个区间的操作次数,除了 \(a[m,2m]\),而该区间的操作次数能由 \(c-a_m-a_{2m+1}\) 计算。
此时 \(a[1,m],a[2m+1,3m]\) 已经满足条件,只要判断 \(a[m+1,2m]\) 是否正确。
注意到我们总有 \(a_i+a_{i+m}+a_{i+2m}=c\),因此可以写出所有判定条件:
- \(a[1,m]\) 递增,\(a[2m+1,3m]\) 递减$。
- \(a_m+a_{2m+1}<c\)。
- \(a_i+a_{m+i}+a_{2m+i}=c\)。
根据这三个条件不难设计出 dp:枚举 \(a_{2m+1}\),然后 \(f_{i,x,y}\) 表示 \(a_{i}=x,a_{2m+i}=y\) 的方案数。
此时可以做到 \(\mathcal O(nC^5)\),注意到 \((x,y)\to(x',y')\) 可以分步转移 \((x,y)\to (x,y')\to (x',y')\) 优化到 \(\mathcal O(nC^4)\)。
但 \(C\) 太大了做不了,注意到根据 \(2^m\) 做法的结论,答案是一个关于 \(c\) 的 \(n\) 次多项式,于是对 \(c=1\sim n+1\) 算出答案后拉格朗日插值即可。
时间复杂度 \(\mathcal O(n^32^{n/3}+n^6)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
#define pc __builtin_popcount
using namespace std;
const int MOD=998244353;
inline void add(ll &x,const ll &y) { x=(x+y)%MOD; }
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; }
int n,m;
ll c,b[55],sb[55],pw[55][55];
ll qb(int l,int r) { return (sb[l]+MOD-sb[max(0,r-m)])%MOD; }
signed main() {
scanf("%d%d%lld",&n,&m,&c);
ll inv=0,midv=1;
for(int i=1;i<=n-m+1;++i) scanf("%lld",&b[i]),inv+=b[i];
inv=ksm(inv);
for(int i=1;i<=n-m+1;++i) b[i]=b[i]*inv%MOD;
for(int i=1;i<=n;++i) sb[i]=(sb[i-1]+b[i])%MOD;
if(2*m>n) {
int k=2*m-n;
midv=ksm(c,k),n-=k,m-=k;
}
if(m==1) {
ll pr=1;
for(int i=1;i<=n;++i) pr=pr*(c-i+1)%MOD*b[i]%MOD;
printf("%lld\n",pr*midv%MOD);
return 0;
}
if(m<=16) {
static ll f[1<<15][55],g[1<<15][55];
memset(f,0,sizeof(f)),f[0][0]=1;
for(int i=1;i<=n;++i) {
memset(g,0,sizeof(g));
for(int s=0;s<(1<<(m-1));++s) for(int j=0;j<i;++j) if(f[s][j]) {
if(s&1) { add(g[s>>1][j],f[s][j]*qb(i-m+1,i)); continue; }
add(g[s>>1][j],f[s][j]*pc(s));
for(int k=1;k<m;++k) if(s>>k&1) {
add(g[(s^(1<<k))>>1][j],f[s][j]*qb(i-m+1+k,i));
}
add(g[s>>1|1<<(m-2)][j+1],f[s][j]);
add(g[s>>1][j+1],f[s][j]*qb(i,i));
}
memcpy(f,g,sizeof(f));
}
ll ans=0,pr=1;
for(int i=0;i<=n;++i) add(ans,pr*f[0][i]),pr=pr*(c-i)%MOD;
printf("%lld\n",ans*midv%MOD);
return 0;
}
int lim=n+2;
for(int i=1;i<=n+1;++i) for(int j=pw[i][0]=1;j<=lim;++j) pw[i][j]=pw[i][j-1]*b[i]%MOD*ksm(j)%MOD;
static ll vl[55],f[55][55],g[55][55];
memset(vl,0,sizeof(vl));
for(int C=1;C<=lim;++C) {
auto a=[&](int i,ll x)->ll { return i<=n?x:1; };
for(int w=0;w<=C;++w) {
memset(f,0,sizeof(f));
for(int i=0;i<=C-w;++i) {
f[i][w]=a(1,i)*a(m+1,C-w-i)*a(2*m+1,w)%MOD*pw[1][i]%MOD;
}
for(int i=2;i<=m;++i) {
memset(g,0,sizeof(g));
for(int j=0;j<=C-w;++j) for(int k=0;k<=w;++k) if(f[j][k]) {
for(int nj=j;nj<=C-w;++nj) {
add(g[nj][k],f[j][k]*pw[i][nj-j]);
}
}
memset(f,0,sizeof(f));
for(int j=0;j<=C-w;++j) for(int k=0;k<=w;++k) if(g[j][k]) {
for(int nk=0;nk<=k;++nk) {
add(f[j][nk],g[j][k]*pw[m+i][k-nk]%MOD*a(i,j)*a(m+i,C-j-nk)*a(2*m+i,nk));
}
}
}
for(int j=0;j<=C-w;++j) for(int k=0;k<=w;++k) if(f[j][k]){
add(vl[C],f[j][k]*pw[2*m+1][k]%MOD*pw[m+1][C-j-w]);
}
}
for(int i=1;i<=C;++i) vl[C]=vl[C]*i%MOD;
}
ll ans=0;
for(int i=1;i<=lim;++i) {
ll x=vl[i],y=1;
for(int j=1;j<=lim;++j) if(j!=i) x=x*(c+MOD-j)%MOD,y=y*(i+MOD-j)%MOD;
add(ans,x*ksm(y));
}
printf("%lld\n",ans*midv%MOD);
return 0;
}
*C. 树论(tree)
题目大意
对于两棵树 \(T_1,T_2\),定义 \(f_i(S)\) 表示 \(S\) 在 \(T_i\) 上的导出子图的叶子个数。
定义 \(T_1\preccurlyeq T_2\) 当且仅当 \(\forall S_2\subseteq V\) 均存在 \(S_2\subseteq S_1\) 使得 \(f_2(S_2)\ge f_1(S_1)\)。
如果 \(T_1\preccurlyeq T_2\) 且 \(T_2\preccurlyeq T_1\),那么认为 \(T_1,T_2\) 等价。
给定 \(k\) 棵树 \(T_1\sim T_k\),求 \(T_1\sim T_k\preccurlyeq T\) 的 \(T\) 的数量,以及 \(T\preccurlyeq T_1\sim T_k\) 的 \(T\) 构成的等价类数量。
数据范围:\(n\le 5000,k\le 1000\)。
思路分析
先分析题目中给出的这个偏序关系:
用 \(V_i(S)\) 表示 \(S\) 在 \(T_i\) 上的斯坦纳树点集,首先我们观察 \(f_i(S)\) 的超集最小值,一个自然的观察是最小值在 \(V_i(S)\) 取到。
证明是简单的,考虑 \(V_i(S)\) 上的每个叶子,一定是 \(S\) 的叶子,那么 \(S\) 的每个超集一定包含这些叶子,在每个叶子以及其外面的点中至少有一个叶子,因此 \(f_i(S)\) 的超集最小值就是 \(f_i(V_i(S))\),记为 \(g_i(S)\)。
那么限制就变成 \(f_2(S)\ge g_1(S)\),注意到 \(g\) 有单调性,即 \(S\subseteq T \implies g(S)\le g(T)\),证明就是上面的过程,\(g(S)\subseteq g(T)\),而 \(g(S)\) 是 \(f(S)\) 的超集最小值。
那么我们把 \(S\) 变成 \(V_2(S)\),会减小右侧限制增大左侧限制,那么只要在 \(T_2\) 上联通的 \(S\) 是否有 \(f_2(S)\ge g_1(S)\) 即可。
一个核心观察是,我们只要考虑 \(f_2(S)=2\) 的点合法,那么就能得到 \(T_1\preccurlyeq T_2\),证明如下:
首先 \(f_2(S)=1\) 的点显然总是成立。
然后用数学归纳法,我们已知 \(f_2(S)\le k\) 的 \(S\) 都合法,现在要证明 \(f_2(S)=k+1\) 的 \(S\) 都合法。
不妨假设 \(S'\) 是 \(S\) 中选取 \(k\) 个叶子生成的斯坦纳树,则 \(k=f_2(S')\ge g_1(S')\)。
观察 \(S\setminus S'\),其形态是一条过某个叶子 \(x\) 的链,取一个 \(S'\) 中的叶子 \(y\),则 \(S=S'\cup V_2(\{x,y\})\)。
记 \(C=V_2(\{x,y\})\),由于 \(f_2(C)=2\),因此 \(g_1(C)\le f_2(C)=2\),那么 \(V_1(C)\) 也是一条链。
因为 \(y\in v_1(S')\cap V_1(C)\),所以 \(V_1(S)=V_1(S')\cup V_1(C)\),从而 \(g_1(S)\le g_1(S')+g_1(C)\le k+2\)。
现在要证明 \(V_1(S')\) 并上 \(V_1(C)\) 的过程能删掉 \(V_1(S’)\) 中的一个叶子。
这是简单的,考虑离 \(x\) 最近的叶子 \(z\),由于 \(y\in V_1(S')\),那么 \(z\in V_1(C)\),则 \(z\) 在 \(V_1(S)\) 中不是叶子。
所以 \(g_1(S)\le k+1\),证毕。
那么 \(T_1\preccurlyeq T_2\) 当且仅当 \(T_2\) 中每条链在 \(T_1\) 中也是一条链的子集。
我们有一种好的方法刻画这个限制,定义 \(G(T)\) 为 \(\{(u,v)\mid g(u,v)=2\}\),即将 \(T\) 中的每个链变成一个团,此时 \(T_1\preccurlyeq T_2\iff G(T_2)\subseteq G(T_1)\)。
证明如下:
首先考虑充分性,建出 \(G\) 的圆方树,此时 \(T_1\) 上的一条链在其圆方树上也是一条链,而 \(G(T_2)\) 的圆方树可以由 \(G(T_1)\) 的圆方树合并若干方点得到,则其在 \(T_2\) 上也是一条链。
否则考虑 \((u,v)\in G(T_2)\setminus G(T_1)\),此时 \(V_2(\{u,v\})\) 在 \(T_2\) 上是一条无分支的链,那么对于任意 \(w\),\(g_2(\{u,v,w\})=2\)。
由于 \((u,v)\not\in G(T_1)\),因此 \(u,v\) 路径上存在一个非二度点,取他的另一个邻居 \(w\),则 \(g_1(\{u,v,w\})\ge 3\)。
选取点集 \(V_2(\{u,v,w\})\) 即导出矛盾。证毕。
那么先解决问题二,bitset 求出 \(G=\bigcap G(T_i)\),则 \(G(T)\subseteq G\)。
首先 \(G\) 必须连通,对 \(G\) 建立圆方树,容易证明每个点双连通分量都是团,那么限制就是 \(G\) 每个点双连通分量在 \(T\) 连通,对每个点双求生成树个数乘起来。
然后是问题一,依然求 \(G=\bigcup G(T_i)\) 的圆方树,这个不用 bitset,把 \(G(T_i)\) 中的团当成环,然后求圆方树即可。
现在我们有一棵圆方树,可能的操作就是合并一些相邻的方点,要求是:每个圆点度数 \(\ne 2\),每个方点至多和两个非叶节点相连。
可以考虑圆方树上 dp,\(f_{u,i}\) 表示 \(u\) 所在的方点中有 \(i\) 个非叶节点,其中 \(i\in[0,2]\),具体来说:
- 如果 \(u\) 是方点,那么 \(i\) 表示 \(u\) 所在的新方点有 \(i\) 个非叶节点。
- 如果 \(u\) 是圆点,那么 \(i\) 表示 \(fa(u)\) 所在的新方点会增加 \(i\) 个非叶节点(可能把 \(u\) 的儿子和 \(fa(u)\) 合并)。
那么对于方点的转移,直接子树卷积即可。
对于圆点,我们分析儿子的方点会合并成什么结构,首先如果合并出一个有两个非叶节点的方点,那么 \(u\) 必须是叶子,即此时 \(u\) 的所有邻居都得合并。
所以我们先子树卷积算出 \(\sum i=2\) 的方案。
剩下的情况,我们用 \(g_{x,y}\) 表示合并出 \(x\) 个没有非叶节点的方点,\(y\) 个恰有一个非叶节点的方点。
然后考虑转移:
- \(x+y=1\):此时儿子的方点必须和 \(fa(u)\) 合并,否则 \(\deg(u)=2\),转移到 \(f_{u,y}\)。
- \(x+y=2\):此时儿子的方点不能和 \(fa(u)\) 合并,转移到 \(f_{u,1}\)。
- \(x+y>2\):可以任意选择是否有儿子和 \(fa(u)\) 合并:
- 不合并:\(g_{x,y}\to f_{u,1}\)。
- 合并一个零度点:\(xg_{x,y}\to f_{u,1}\)。
- 合并一个一度点:\(yg_{x,y}\to f_{u,2}\)。
暴力转移 \(g_{x,y}\) 的复杂度是 \(\mathcal O(n^3)\) 的。
注意到 \(x+y\le 2\) 的转移是平凡的,我们只要维护 \(\sum g_{x,y},\sum xg_{x,y},\sum yg_{x,y}\),然后对于 \(x+y\le 2\) 的点重算贡献。
直接用组合计数的方法维护,\(h_i\) 表示 \(u\) 子树中恰有 \(i\) 个儿子有一个非叶子节点。
设 \(u\) 共有 \(d\) 个儿子,枚举有 \(j\) 个零度点和他们合并,方案数为 \(h_{i}\times\binom{d-i}j\times i^j\),对于 \(g_{k,i}\) 的贡献就是 \(h_{i}\times\binom{d-i}j\times i^j\times\begin{Bmatrix}d-i-j\\k\end{Bmatrix}\)。
因此维护答案只要预处理第二类斯特林数的行和,以及每行 \(\times j\) 的和。
时间复杂度 \(\mathcal O\left(nk+n^2+\dfrac{n^2k}{\omega}\right)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5005,MOD=998244353;
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; }
int ty,q,n;
bitset <MAXN> ch,U[MAXN],V[MAXN];
vector <int> G[MAXN],E[MAXN*2];
int st[MAXN],tp,dfn[MAXN],low[MAXN],dcnt,vc;
bool ins[MAXN];
void dfs0(int u,int fz) {
ch.set(u),st[++tp]=u;
if(G[u].size()==2) return dfs0(G[u][0]^G[u][1]^fz,u);
if(fz) {
if(ty==0) {
for(int i=1;i<=tp;++i) V[st[i]][st[i%tp+1]]=V[st[i%tp+1]][st[i]]=1;
tp=0;
} else {
while(tp) U[st[tp--]]|=ch;
ch.reset();
}
}
for(int v:G[u]) if(v^fz) ch.set(u),st[++tp]=u,dfs0(v,u);
}
void link(int u,int v) { E[u].push_back(v),E[v].push_back(u); }
void tarjan(int u,int fz) {
dfn[u]=low[u]=++dcnt,st[++tp]=u,ins[u]=true;
for(int v:G[u]) if(v^fz) {
if(!dfn[v]) {
tarjan(v,u),low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]) {
link(++vc,u);
while(ins[v]) link(vc,st[tp]),ins[st[tp--]]=false;
}
} else low[u]=min(low[u],dfn[v]);
}
}
int C[MAXN][MAXN],pw[MAXN][MAXN];
int S[MAXN][MAXN],s1[MAXN],s2[MAXN];
ll f[MAXN*2][3],g[4][4],h[MAXN];
void dfs1(int u,int fz) {
int m=0; f[u][0]=1;
for(int v:E[u]) if(v^fz) {
dfs1(v,u),++m;
f[u][2]=(f[u][2]*f[v][0]+f[u][1]*f[v][1]+f[u][0]*f[v][2])%MOD;
f[u][1]=(f[u][1]*f[v][0]+f[u][0]*f[v][1])%MOD,f[u][0]=f[u][0]*f[v][0]%MOD;
}
if(u>n||!m) return ;
f[u][0]=f[u][1]=0;
memset(g,0,sizeof(g)),g[0][0]=1;
memset(h,0,sizeof(h)),h[0]=1;
for(int v:E[u]) if(v^fz) {
ll x=f[v][0],y=f[v][1];
for(int i=2;i>=0;--i) for(int j=2-i;j>=0;--j) if(g[i][j]) {
g[i+1][j]=(g[i+1][j]+g[i][j]*y)%MOD;
g[i][j+1]=(g[i][j+1]+g[i][j]*x)%MOD;
if(j>0) g[i+1][j-1]=(g[i+1][j-1]+g[i][j]*y%MOD*j)%MOD;
g[i][j]=g[i][j]*x%MOD*(i+j)%MOD;
}
for(int i=m;i>=0;--i) if(h[i]) h[i+1]=(h[i+1]+h[i]*y)%MOD,h[i]=h[i]*x%MOD;
}
for(int i=0;i<=m;++i) for(int j=0;i+j<=m;++j) {
ll z=h[i]*C[m-i][j]%MOD*pw[i][j]%MOD;
f[u][1]=(f[u][1]+(s1[m-i-j]+s2[m-i-j])*z)%MOD;
f[u][2]=(f[u][2]+1ll*s1[m-i-j]*i%MOD*z)%MOD;
}
for(int i=0;i<=2;++i) for(int j=0;i+j<=2;++j) if(g[i][j]) {
if(i+j==1) f[u][i]=(f[u][i]+g[i][j])%MOD;
else if(i+j==2) f[u][1]=(f[u][1]+g[i][j])%MOD;
f[u][1]=(f[u][1]+(MOD-g[i][j])*(j+1))%MOD;
f[u][2]=(f[u][2]+(MOD-g[i][j])*i)%MOD;
}
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>ty>>q>>n;
if(ty==1) for(int i=1;i<=n;++i) V[i].set();
while(q--) {
for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
for(int i=1;i<=n;++i) if(G[i].size()!=2) { dfs0(i,0); break; }
if(ty==1) for(int i=1;i<=n;++i) V[i]&=U[i],U[i].reset();
for(int i=1;i<=n;++i) G[i].clear();
}
for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) if(i!=j&&V[i][j]) G[i].push_back(j);
vc=n,tarjan(1,0);
if(ty==1) {
for(int i=1;i<=n;++i) if(!dfn[i]) return cout<<"0\n",0;
ll ans=1;
for(int i=n+1;i<=vc;++i) if(E[i].size()>1) ans=ans*ksm(E[i].size(),E[i].size()-2)%MOD;
return cout<<ans<<"\n",0;
}
for(int i=0;i<=n;++i) for(int j=C[i][0]=1;j<=i;++j) C[i][j]=(C[i-1][j]+C[i-1][j-1])%MOD;
for(int i=0;i<=n;++i) for(int j=pw[i][0]=1;j<=n;++j) pw[i][j]=1ll*pw[i][j-1]*i%MOD;
for(int i=0;i<=n;++i) {
S[i][0]=!i;
for(int j=1;j<=i;++j) S[i][j]=(1ll*S[i-1][j]*j+S[i-1][j-1])%MOD;
for(int j=0;j<=i;++j) s1[i]=(s1[i]+S[i][j])%MOD,s2[i]=(s2[i]+1ll*S[i][j]*j)%MOD;
}
for(int i=1;i<=n;++i) if(E[i].size()==1) {
dfs1(i,0),cout<<(f[i][0]+f[i][1]+f[i][2])%MOD<<"\n";
return 0;
}
return 0;
}
Round #48 - 20250209
A. 硬币(coin)
题目大意
给定 \(n\) 个点 \(m\) 条边的 DAG,对于每个 \(u\) 求最小的 \(i\) 使得保留前 \(i\) 条边后 \(u\) 的拓扑序唯一。
数据范围:\(n\le 2\times 10^5,m\le 8\times 10^5\)。
思路分析
首先考虑什么样的 \(u\) 拓扑序唯一,即拓扑序小于 \(u\) 的点都能到达 \(u\),大于 \(u\) 的点都能被 \(u\) 到达。
考虑第一个条件,只保留拓扑序小等于 \(u\) 的点,那么我们要求 \(u\) 是唯一一个无出度的点。
同理保留拓扑序大于等于 \(u\) 的点,\(u\) 是唯一一个无入度的点,这两个条件显然是充要的。
那么动态维护前缀或后缀的导出子图上每个点的最小出边,用堆维护答案即可。
时间复杂度 \(\mathcal O(m\log m)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5,MAXM=8e5+5;
int n,m,a[MAXN],deg[MAXN];
struct Edge { int v,w; };
vector <Edge> L[MAXN],R[MAXN];
int f[MAXN],g[MAXN];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1,u,v;i<=m;++i) {
cin>>u>>v,++deg[v],L[u].push_back({v,i}),R[v].push_back({u,i});
}
queue <int> Q;
for(int i=1;i<=n;++i) if(!deg[i]) Q.push(i);
for(int i=1;i<=n;++i) {
int u=a[i]=Q.front(); Q.pop();
for(auto e:L[u]) if(!--deg[e.v]) Q.push(e.v);
}
priority_queue <array<int,2>> S;
for(int i=n;i>=1;--i) {
int u=a[i];
for(auto e:L[u]) if(e.w<g[e.v]) S.push({g[e.v]=e.w,e.v});
while(S.size()) {
auto it=S.top();
if(it[0]!=g[it[1]]) S.pop();
else { f[u]=max(f[u],it[0]); break; }
}
S.push({g[u]=m+1,u});
}
priority_queue<array<int,2>>().swap(S);
for(int i=1;i<=n;++i) {
int u=a[i];
for(auto e:R[u]) if(e.w<g[e.v]) S.push({g[e.v]=e.w,e.v});
while(S.size()) {
auto it=S.top();
if(it[0]!=g[it[1]]) S.pop();
else { f[u]=max(f[u],it[0]); break; }
}
S.push({g[u]=m+1,u});
}
for(int i=1;i<=n;++i) cout<<(f[i]>m?-1:f[i])<<" \n"[i==n];
return 0;
}
*B. 病毒(virus)
题目大意
给定 \(n\times m\) 网格,以及长度为 \(k\) 的字符串 \(S\)(字符集东南西北),初始每个格子为白色。
在第 \(t\) 个时刻,我们选取方向 \(S^{\infty}_t\),然后取出每个格子在该方向的邻居,如果某个格子 \((i,j)\) 连续 \(a_{i,j}\) 个时刻都取到了一个黑色的邻居,那么这个格子也染黑。
你要选择一个格子染黑,最小化充分大时间后的黑色格子数量,并且求出有多少个取到最小值的起点。
数据范围:\(n,m\le 800,k\le 10^5\)。
思路分析
首先我们要快速判定一个格子是否能染黑,取出其所有黑色邻居所在的方向,记为集合 \(s\),那么要求就是 \(S^{\infty}\) 中存在一个长度 \(\ge a_{i,j}\) 的连续段全部由 \(s\) 中字符构成。
对每个 \(s\) 预处理出 \(S^{\infty}\) 中由 \(s\) 构成的极长连续段,可以 \(\mathcal O(1)\) 判定每个格子是否染黑。
那么暴力就是从每个点开始 bfs,但显然无法通过。
考虑利用一些已有的信息,例如从 \(u\) 出发能染黑 \(v\),那么从 \(v\) 出发能染黑的点从 \(u\) 出发一定也会染黑。
因此可以考虑连边 \(u\to v\),则每个点的答案就是后继个数。
先对这张图缩点,那么答案只可能是无出度的强连通分量。
可以发现如果存在一条边 \(u\to v\),那么 \(u\) 的答案不优于 \(v\) 的答案。
因此我们取出这张图的一个内向生成森林,则答案一定来自每个根所在的强连通分量。
如果求出极大的一个内向生成森林,显然一个强连通分量内不会有两个根,因此对每个根暴力 bfs 复杂度正确。
那么问题就变成了在这张图上求出一个极大内向生成森林。
稠密图上的生成树问题可以考虑 Boruvka,在这题中,我们动态维护一个内向生成森林,然后对每个根找一条连向其他树的出边,然后以任意顺序加入这条边(只要不成环)。
很显然每轮增广后连通块数量减半,那么增广轮数是 \(\mathcal O(\log nm)\) 的。
每轮增广就从每个根开始 bfs,如果遇到一个和自己不同色的点就立即退出,那么每个点只会被同色的根 bfs 到,复杂度 \(\mathcal O(nm)\)。
时间复杂度 \(\mathcal O(k+nm\log nm)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=805,MAXL=2e5+5,MAXV=1e6+5,inf=1e9,dx[]={-1,1,0,0},dy[]={0,0,-1,1};
char op[MAXL];
int n,m,q,lim[16],a[MAXN][MAXN];
int cl[MAXN][MAXN],id[MAXN][MAXN],dsu[MAXV],tot;
bool vis[MAXN][MAXN];
int ty(char c) { return c=='N'?0:(c=='S'?1:(c=='W'?2:3)); }
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>q>>n>>m;
for(int i=1;i<=q;++i) cin>>op[i],op[i+q]=op[i];
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) cin>>a[i][j],id[i][j]=++tot;
for(int s=1;s<16;++s) {
for(int i=1,j=lim[s]=-1;i<=2*q;++i) if(!(s>>ty(op[i])&1)) {
if(~j) lim[s]=max(lim[s],i-j-1); j=i;
}
if(lim[s]==-1) lim[s]=inf;
}
iota(dsu+1,dsu+tot+1,1);
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) if(a[i][j]) cl[i][j]=id[i][j];
while(true) {
memset(vis,false,sizeof(vis));
vector <array<int,2>> E;
int ans=inf,cnt=0;
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) if(cl[i][j]==id[i][j]) {
int z=0; queue <array<int,2>> Q;
Q.push({i,j}),vis[i][j]=true;
auto is=[&](int x,int y){ return vis[x][y]&&cl[x][y]==cl[i][j]; };
while(Q.size()) {
int u=Q.front()[0],v=Q.front()[1]; Q.pop(),++z;
for(int d:{0,1,2,3}) {
int x=u+dx[d],y=v+dy[d];
if(!a[x][y]||is(x,y)) continue;
int s=is(x-1,y)|is(x+1,y)<<1|is(x,y-1)<<2|is(x,y+1)<<3;
if(a[x][y]<=lim[s]) {
if(cl[x][y]==cl[i][j]) Q.push({x,y}),vis[x][y]=true;
else { E.push_back({cl[i][j],cl[x][y]}); goto _; }
}
}
}
if(ans>z) ans=cnt=z;
else if(ans==z) cnt+=z;
_:;
}
if(E.empty()) return cout<<ans<<"\n"<<cnt<<"\n",0;
for(auto e:E) if(find(e[1])!=e[0]) dsu[e[0]]=find(e[1]);
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) if(a[i][j]) cl[i][j]=find(id[i][j]);
}
return 0;
}
*C. 池塘(pond)
题目大意
数轴上给定 \(n\) 个点 \(x_1\sim x_n\),从 \(x_k\) 出发访问所有点,最小化每个点首次被访问时经过的距离总和。
数据范围:\(n\le 3\times 10^5\)。
思路分析
朴素的状态是 \(dp_{l,r,0/1}\) 表示当前经过 \([l,r]\) ,到达 \(l/r\) 时的最小总和。
我们需要在状态中分离出 \(l,r\),即对每个 \(l/r\) 分别计算贡献。
尝试再每个掉头的位置计算贡献,对每个点我们先假设答案是 \(|x_i-x_k|\),此时如果我们在到达 \(x_l\) 之后先掉头去了 \(x_r\) 再回到 \(x_{l-1}\),那么就会让 \(x_1\sim x_{l-1}\) 的距离增加 \(2|x_l-x_r|\)。
那么就能设立出如下状态:\(f_l,g_r\) 表示掉头在 \(x_l/x_r\) 时的最小代价,转移就是 \(g_r=\min f_l+2(l-1)|x_r-x_l|,f_l=\min g_r+2(n-r)|x_r-x_l|\)。
但是此时我们对 \(f,g\) 的转移没有一个明确的顺序,并且我们并不能保证 dp 出来的路径满足 \(l\) 递减 \(r\) 递增,
分析该转移,首先我们可以用 Dijkstra 维护转移。
其次我们注意到 \(g_r\to f_l\) 的代价严格小于 \(g_r\to f_{l-1}\) 的代价。
因此我们 Dijkstra 从 \([u,v]=[k,k]\) 开始,每一次增广到的最近点一定是 \(x_{u-1}/x_{v-1}\),即任何时候经过的点都是一个区间。
由于我们的代价函数形式较好,不难证明 \(f,g\) 的转移具有决策单调性,从而 dp 出的路径一定是合法的。
那么我们对已知的 \(f/g\) 维护一个凸壳,转移的时候直接求最小点值,可以用李超树实现。
时间复杂度 \(\mathcal O(n\log V)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5;
const ll inf=1e18;
struct func {
ll k,b;
ll f(ll x) { return k*x+b; }
} a[MAXN];
struct LCT {
int tr[MAXN],ls[MAXN],rs[MAXN],tot;
void ins(int id,ll l,ll r,int &p) {
if(!p) return tr[p=++tot]=id,void();
ll mid=(l+r)>>1;
if(a[tr[p]].f(mid)>a[id].f(mid)) swap(tr[p],id);
if(l==r) return ;
if(a[tr[p]].f(l)>a[id].f(l)) ins(id,l,mid,ls[p]);
if(a[tr[p]].f(r)>a[id].f(r)) ins(id,mid+1,r,rs[p]);
}
ll qry(ll x,ll l,ll r,int p) {
ll s=a[tr[p]].f(x);
if(!p||l==r) return s;
ll mid=(l+r)>>1;
return min(s,x<=mid?qry(x,l,mid,ls[p]):qry(x,mid+1,r,rs[p]));
}
} F,G;
int n,m,rf,rg;
ll x[MAXN],f[MAXN],g[MAXN];
signed main() {
scanf("%d%d",&n,&m),a[0]={0,inf};
for(int i=2;i<=n;++i) scanf("%lld",&x[i]),x[i]+=x[i-1];
a[n+1]={-2*(n-m),2*(n-m)*x[m]},F.ins(n+1,0,x[n],rf);
a[n+2]={2*(m-1),-2*(m-1)*x[m]},G.ins(n+2,0,x[n],rg);
int l=m,r=m;
while(1<l||r<n) {
ll vl=(l==1?inf:F.qry(x[l-1],0,x[n],rf));
ll vr=(r==n?inf:G.qry(x[r+1],0,x[n],rg));
if(vl<vr) {
f[--l]=vl,a[l]={2*(l-1),vl-2*(l-1)*x[l]},G.ins(l,0,x[n],rg);
} else {
g[++r]=vr,a[r]={-2*(n-r),vr+2*(n-r)*x[r]},F.ins(r,0,x[n],rf);
}
}
ll ans=min(f[1],g[n]);
for(int i=1;i<=n;++i) ans+=abs(x[i]-x[m]);
cout<<ans<<"\n";
return 0;
}
Round #49 - 20250211
A. 冰(ice)
题目大意
给定 \(n\times m\) 网格,初始每个格子上有一个金币,你每次可以捡起所在网格的金币,要求你身上的金币数量加上当前格子 \((i,j)\) 的金币数量 \(\le a_{i,j}\),你要从网格的边界走到边界,最大化捡起的金币数。
数据范围:\(n\times m\le 2\times 10^5\)。
思路分析
先假设当前格子的金币数量恒为 \(1\),相当于每次只能经过 \(a_{i,j}>x\) 的点,直到捡起下一个金币。
建立 Kruskal 重构树,但这个过程中我们可以走到的点集不断缩小,难以刻画。
可以先二分答案 \(k\),然后时光倒流,那么枚举终点 \(u\),我们每次能走到的点集严格扩大。
枚举 \(x\),求出 \(u\) 权值 \(>x\) 的最远祖先 \(v\),判断 \(v\) 子树大小是否 \(\ge k-x+1\) 即可。
容易发现如果 \(v\) 是 \(u\) 的祖先,那么对答案的限制就是 \(k\le siz_v+w_v-1\),其中 \(w_v\) 是能取到 \(v\) 的最小 \(x\),即 \(u\to v\) 路径上一个点的点权,要求 \(a_v\ne w_v\)。
那么我们对每个 \(v\) 处理出对 \(k\) 的限制,然后每个 \(u\) 的答案就是祖先最小值,处理这个过程是线性的。
现在我们还要考虑取走金币后限制会减小,倒流来看就是 \(a_{fa(v)}=x\) 则还能到达 \(x\),此时 \(v\) 对答案的限制变成 \(k\le siz_v+w_v\)。
时间复杂度 \(\mathcal O(nm\log nm)\),瓶颈在排序。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5,dx[]={-1,1,0,0},dy[]={0,0,1,-1};
int n,h,w,a[MAXN],dsu[MAXN],fa[MAXN],siz[MAXN],lim[MAXN];
bool vis[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
vector <int> id[MAXN],src,G[MAXN],ord;
signed main() {
scanf("%d%d",&h,&w);
for(int i=1;i<=h;++i) {
id[i].resize(w+1);
for(int j=1;j<=w;++j) id[i][j]=++n,scanf("%d",&a[n]);
}
for(int i=1;i<=h;++i) for(int j=1;j<=w;++j) {
int u=id[i][j]; ord.push_back(u);
if(i>1) G[u].push_back(id[i-1][j]);
if(j>1) G[u].push_back(id[i][j-1]);
if(i<h) G[u].push_back(id[i+1][j]);
if(j<w) G[u].push_back(id[i][j+1]);
if(G[u].size()<4) src.push_back(u);
}
iota(dsu+1,dsu+n+1,1);
sort(ord.begin(),ord.end(),[&](int i,int j){ return a[i]>a[j]; });
for(int u:ord) {
vis[u]=true;
for(int v:G[u]) if(vis[v]&&find(v)!=u) fa[find(v)]=u,dsu[find(v)]=u;
}
for(int u:ord) if(fa[u]) siz[fa[u]]+=++siz[u];
for(int x=1;x<=n;++x) lim[x]=(a[x]!=a[fa[x]]?siz[x]+a[fa[x]]:n);
for(int i=n-2;~i;--i) lim[ord[i]]=min(lim[ord[i]],lim[fa[ord[i]]]);
int ans=0;
for(int u:src) ans=max(ans,min(a[u],lim[u]));
printf("%d\n",ans);
return 0;
}
B. 矩形(rect)
题目大意
给定一个 \(n\) 行棋盘,第 \(i\) 行宽度 \(a_i\),对于 \(k\in[1,3]\) 求出在棋盘中选择 \(k\) 个不相交矩形的最大面积和。
数据范围:\(n\le 5\times 10^5\)。
思路分析
先从 \(k=1\) 开始,设高度为 \(a_i\),那么我们会贪心选出极长的 \(\ge a_i\) 的段作为底。
因此答案一定是笛卡尔树上的子树,即答案为 \(\max a_x\times s_x\),其中 \(s_x\) 表示子树大小。
然后是 \(k=2\),如果选择的两个矩形底不交,很显然就是笛卡尔树上选两个没有祖先关系的点,容易 dp 解决。
如果选择的矩形底有交,类似上面,我们选出的两个矩形可以被笛卡尔树上的一对祖孙刻画,即 \(\max (a_x-a_y)s_x+a_y\times s_y\),其中 \(y\) 是 \(x\) 的祖先。
枚举 \(y\),相当维护子树的凸壳,可以用线段树套李超线段树处理。
最后解决 \(k=3\),如果可以分割成 \(k=1\) 或 \(k=2\) 的子问题是平凡的。
还剩两种情况,分别是选择的三个点构成一条祖孙链 \(x\to y\to z\),或者 \(x,y\) 同时是 \(z\) 的不交后代。
第一种情况类似 \(k=2\) 的情况,在 \(y\) 处求出最大的 \(x\),然后塞进另一个树套树里为 \(z\) 维护答案。
现在只要处理 \(x,y\) 都是 \(z\) 不交后代,答案为 \(\max(a_x-a_z)s_x+(a_y-a_z)s_y+a_zs_z\)。
如果 \(z=\mathrm{LCA}(x,y)\),这是简单的,由于笛卡尔树是二叉树,我们在两个子树用刚才的树套树分别求出最优的 \(x,y\) 即可。
进一步,在此处对于任意一个 \(z\),都可以 \(\mathcal O(\log^2n)\) 求出答案。
但是我们显然不能遍历所有 \(z\),可以尝试一些乱搞,例如只枚举 \(\mathrm{LCA}(x,y)\) 向上最近的 \(B\) 个权值不同的祖先 \(z\),取 \(B=8\) 时可以通过此题。
时间复杂度 \(\mathcal O(nB\log^2n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e5+5,V=5e5;
struct func {
ll k,b;
ll f(ll x) { return k*x+b; }
} e[MAXN*2];
struct LCT {
int tr[MAXN*40],ls[MAXN*40],rs[MAXN*40],tot;
void ins(int id,ll l,ll r,int &p) {
if(!p) return tr[p=++tot]=id,void();
ll mid=(l+r)>>1;
if(e[tr[p]].f(mid)<e[id].f(mid)) swap(tr[p],id);
if(l==r) return ;
if(e[tr[p]].f(l)<e[id].f(l)) ins(id,l,mid,ls[p]);
if(e[tr[p]].f(r)<e[id].f(r)) ins(id,mid+1,r,rs[p]);
}
ll qry(ll x,ll l,ll r,int p) {
ll s=e[tr[p]].f(x);
if(!p||l==r) return s;
ll mid=(l+r)>>1;
return max(s,x<=mid?qry(x,l,mid,ls[p]):qry(x,mid+1,r,rs[p]));
}
} T;
struct zkwSegt {
static const int N=1<<19;
int rt[N<<1];
void ins(int x,int id) {
for(x+=N;x;x>>=1) T.ins(id,0,V,rt[x]);
}
ll qry(int l,int r,int x) {
ll s=0;
for(l+=N-1,r+=N+1;l^r^1;l>>=1,r>>=1) {
if(~l&1) s=max(s,T.qry(x,0,V,rt[l^1]));
if(r&1) s=max(s,T.qry(x,0,V,rt[r^1]));
}
return s;
}
} T1,T2;
int n,st[MAXN],tp,fa[MAXN],ls[MAXN],rs[MAXN],sz[MAXN],L[MAXN],R[MAXN],dcnt,rg[MAXN],up[MAXN];
ll a[MAXN],f[MAXN],g[MAXN],w[MAXN],dp[MAXN][4];
vector <int> G[MAXN];
vector<ll> ans={0,0,0};
void dfs1(int u,int l,int r) {
L[u]=++dcnt,sz[u]=r-l+1;
if(fa[u]&&a[u]==a[fa[u]]) rg[u]=rg[fa[u]],up[u]=up[fa[u]];
else rg[u]=sz[u],up[u]=fa[u];
if(ls[u]) fa[ls[u]]=u,G[u].push_back(ls[u]),dfs1(ls[u],l,u-1);
if(rs[u]) fa[rs[u]]=u,G[u].push_back(rs[u]),dfs1(rs[u],u+1,r);
R[u]=dcnt;
}
int ec=0;
void dfs2(int u) {
for(int v:G[u]) dfs2(v);
if(ls[u]&&rs[u]) {
e[++ec]={-sz[ls[u]]-sz[rs[u]],f[ls[u]]+f[rs[u]]},T2.ins(L[u],ec);
}
if(L[u]<R[u]) {
g[u]=T1.qry(L[u]+1,R[u],a[u])+a[u]*sz[u];
ans[2]=max(ans[2],T2.qry(L[u]+1,R[u],a[u])+a[u]*sz[u]);
if(ls[u]&&rs[u]) {
for(int x=u,i=0;x&&i<8;++i,x=up[x]) {
ans[2]=max(ans[2],T1.qry(L[ls[u]],R[ls[u]],a[x])+T1.qry(L[rs[u]],R[rs[u]],a[x])+a[x]*rg[x]);
}
}
e[++ec]={-sz[u],g[u]},T2.ins(L[u],ec);
}
f[u]=sz[u]*a[u],e[++ec]={-sz[u],a[u]*sz[u]},T1.ins(L[u],ec);
}
void dfs3(int u) {
for(int v:G[u]) {
dfs3(v),g[u]=max(g[u],g[v]);
ll vl[4]={0,0,0,0};
for(int i=0;i<=3;++i) for(int j=0;i+j<=3;++j) {
vl[i+j]=max(vl[i+j],dp[u][i]+dp[v][j]);
}
swap(dp[u],vl);
}
dp[u][1]=max(dp[u][1],f[u]);
}
vector<ll> max_area(vector<int>H) {
n=H.size();
for(int i=1;i<=n;++i) {
a[i]=H[i-1];
while(tp&&a[i]<a[st[tp]]) ls[i]=st[tp--];
if(tp) rs[st[tp]]=i;
st[++tp]=i;
}
for(int i=1;i<tp;++i) rs[st[i]]=st[i+1];
int rt=st[1];
dfs1(rt,1,n),dfs2(rt),dfs3(rt);
ans[0]=dp[rt][1],ans[1]=max(g[rt],dp[rt][2]),ans[2]=max(ans[2],dp[rt][3]);
for(int x=1;x<=n;++x) if(ls[x]&&rs[x]) {
ans[2]=max({ans[2],dp[ls[x]][1]+g[rs[x]],dp[rs[x]][1]+g[ls[x]]});
}
ans[1]=max(ans[1],ans[0]),ans[2]=max(ans[2],ans[1]);
return ans;
}
*C. 椅子(chair)
题目大意
给定 \(s_0\sim s_{n-1}\),重排 \(s\) 使得 \((i+s_i)\bmod n\) 两两不同,构造方案或报告无解。
数据范围:\(n\le 100\)。
思路分析
以下加减法均在 \(\bmod n\) 意义下考虑。
一个显然的观察是 \(n\mid \sum s_i\),我们可以把题目变成求出两个排列 \(p,q\) 使得 \(p_i+q_i=s_i\)。
我们可以使用调整法构造出解,从 \((p,q,s)\) 调整到另一组解 \((p',q',s')\),满足 \(s\) 与 \(s'\) 的海明距离 \(=2\)。
不妨假设我们修改了 \(s_x,s_y\) 两个位置。
那么我们要调整 \(p,q\),可以想到找一个环 \(r_1\sim r_k\),然后 \(s'_{r_i}=p_{r_{i-1}}+q_{r_{i+1}}\),然后对 \(p,q\) 分别轮换即可。
从 \(r_1=x,r_2=y\) 开始,每次可以计算出新的 \(r_{i+1}\),直到我们遇到两个相同的元素 \(r_j,r_k\)。
如果此时这个 \(i\in\{1,2\}\),那么就可以调整出 \((p',q',s')\),事实上这是可以证明的:
注意到:
\[\begin{aligned} \sum_{i=j}^{k-1} s'_{r_i}&=(p_{r_{j-1}}-p_{r_{k-1}})+(q_{r_k}-q_{r_j})+\sum_{i=j}^{k-1}p_{r_{i}}+q_{r_{i}}\\ \sum_{i=j}^{k-1} s'_{r_i}-s_{r_i}&=p_{r_{j-1}}-p_{r_{k-1}}\\ \end{aligned} \]由于 \(j>2\),那么 \(p_{r_{j-1}}=p_{r_{k-1}}\),与 \((j,k)\) 是首个重复位置矛盾。证毕。
那么用这种方法调整,从 \([0,0,\dots,0]\) 开始,每次让 \(s\) 的一个位置满足要求,另一个位置调整 \(\bmod n\) 余数,然后用上面的构造更新 \(p,q\)。
时间复杂度 \(\mathcal O(n^2)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=105;
int n,S,a[MAXN],b[MAXN],x[MAXN],y[MAXN];
int p[MAXN],st[MAXN],tp;
void solve() {
cin>>n,S=0;
for(int i=0;i<n;++i) cin>>a[i],S+=a[i];
if(S%n) return cout<<"NO\n",void();
cout<<"YES\n";
if(n==1) return cout<<a[0]<<"\n",void();
for(int i=0;i<n;++i) x[i]=i,y[i]=(n-i)%n,b[i]=0;
for(int i=0;i+1<n;++i) {
b[i+1]=(b[i]+n-a[i]%n)%n,b[i]=a[i]%n;
st[1]=i,st[2]=i+1,tp=2;
for(int j=0;j<n;++j) p[y[j]]=j;
while(true) {
st[tp+1]=p[(b[st[tp]]+n-x[st[tp-1]])%n],++tp;
if(st[tp]==i||st[tp]==i+1) break;
}
for(int j=1,v=x[st[tp-1]];j<tp;++j) swap(x[st[j]],v);
int v1=(st[tp]==i+1?y[i]:y[i+1]),v2=y[st[3]];
for(int j=3;j<tp;++j) y[st[j]]=y[st[j+1]];
y[i]=v1,y[i+1]=v2;
}
for(int i=0;i<n;++i) p[(n-x[i])%n]=a[i];
for(int i=0;i<n;++i) cout<<p[i]<<" \n"[i==n-1];
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
Round #50 - 20250213
A. 蜻蜓(fly)
题目大意
给定 \(n\) 个点的树,第 \(i\) 个点上有 \(a_i\) 个颜色为 \(c_i\) 的球。有 \(m\) 个人依次从 \(1\) 走到某个点,每个人会在路径上每个 \(a_i>0\) 的点取一个球,求每个人最终取走了多少种颜色的球。
数据范围:\(n\le 2\times 10^5,m\le 2\times 10^6\)。
思路分析
首先对于每种颜色,仅保留深度最小的一个点产生贡献。
那么一个点有贡献,当且仅当他的所有同色祖先都被清空了。
因此我们维护出每个点在什么时候被清空,那么每个点对答案有贡献的时刻是一个区间,且容易算出。
然后离线下来就变成子树加单点查询。
现在只需要求出每个点的清空时刻,相当于查询子树第 \(a_u\) 小的操作,直接二分得到答案。
时间复杂度 \(\mathcal O(n\log m+m\log n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5,MAXM=2e6+5;
vector <int> G[MAXN],opr[MAXM];
int n,m,a[MAXN],cl[MAXN],id[MAXM],hd[MAXN],pre[MAXM];
int dcnt,dfn[MAXN],rdn[MAXN],rk[MAXN],bc[MAXN],up[MAXN],ql[MAXN],qr[MAXN];
void dfs(int u,int fz) {
dfn[u]=++dcnt,up[u]=bc[cl[u]],bc[cl[u]]=u,rk[dcnt]=u;
for(int v:G[u]) if(v^fz) dfs(v,u);
rdn[u]=dcnt,bc[cl[u]]=up[u];
}
int rt[MAXN];
struct ZkwSegt {;
int ct[MAXM*22],ls[MAXM*22],rs[MAXM*22],a[MAXM*22],tot;
void ins(int u,int l,int r,int q,int &p) {
ct[p=++tot]=ct[q]+1;
if(l==r) return ;
int mid=(l+r)>>1;
if(u<=mid) rs[p]=rs[q],ins(u,l,mid,ls[q],ls[p]);
else ls[p]=ls[q],ins(u,mid+1,r,rs[q],rs[p]);
}
int qry(int k,int l,int r,int q,int p) {
if(l==r) return l;
int mid=(l+r)>>1,sz=ct[ls[p]]-ct[ls[q]];
if(sz>=k) return qry(k,l,mid,ls[q],ls[p]);
return qry(k-sz,mid+1,r,rs[q],rs[p]);
}
} TR;
struct FenwickTree {
int tr[MAXN],s;
void add(int x,int v) { for(;x<=n;x+=x&-x) tr[x]+=v; }
int qry(int x) { for(s=0;x;x&=x-1) s+=tr[x]; return s; }
} FT;
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;++i) cin>>a[i];
for(int i=1;i<=n;++i) cin>>cl[i];
for(int i=1;i<=m;++i) cin>>id[i],pre[i]=hd[id[i]],hd[id[i]]=i;
for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
dfs(1,0);
for(int i=1;i<=n;++i) {
rt[i]=rt[i-1];
for(int x=hd[rk[i]];x;x=pre[x]) TR.ins(x,1,m,rt[i],rt[i]);
}
for(int i=1;i<=n;++i) {
int u=rk[i];
if(!a[u]) qr[u]=0;
else if(TR.ct[rt[rdn[u]]]-TR.ct[rt[dfn[u]-1]]<a[u]) qr[u]=m;
else qr[u]=TR.qry(a[u],1,m,rt[dfn[u]-1],rt[rdn[u]]);
ql[u]=max(ql[up[u]],qr[up[u]]+1);
if(ql[u]<=qr[u]) opr[ql[u]].push_back(u),opr[qr[u]+1].push_back(-u);
}
for(int i=1;i<=m;++i) {
for(int x:opr[i]) {
if(x>0) FT.add(dfn[x],1),FT.add(rdn[x]+1,-1);
else FT.add(dfn[-x],-1),FT.add(rdn[-x]+1,1);
}
cout<<FT.qry(dfn[id[i]])<<" \n"[i==m];
}
return 0;
}
*B. 水果(fruit)
题目大意
给定 \(w_1\sim w_n\),定义一个序列的权值 \(p\) 为所有前缀最大值 \(x\) 处的 \(\sum w_{p_x}\)。
已知排列 \(a\) 中的若干元素,对于每个前缀求其权值的最大值。
数据范围:\(n\le 4\times 10^5\)。
思路分析
朴素 dp 就是 \(f_{i,j}\) 表示 \([1,i]\) 最大值为 \(j\) 的方案数,但此时 \(f\) 中有很多零散的 \(-\infty\),难以维护。
我们把已经在 \(a\) 中出现的 \(j\) 从状态中删掉,然后对于 \(a_i\ne -1\) 的点特殊维护 \(f_{i,a_i}\) 并删除 \(f_i\) 中的一段前缀。
此时 \(f_{i,j}\) 是单调递增的(\(f_{i,j}\) 的解把 \(j\) 换成 \(j+1\) 就得到 \(f_{i,j+1}\) 的解)。
那么对于一个 \(a_i=-1\) 的点,转移就是 \(f_{i,j}\gets\max(f_{i-1,j-1}+v_j,f_{i-1,j})\),其中 \(v_j\) 是第 \(j\) 个 \(\not\in A\) 的数的权值。
直接使用数据结构维护这个过程是不可能的。
但我们发现 \(f_{i,j}-v_j\le f_{i,j-1}\),在最优解处把 \(j\) 换成 \(j-1\) 就能证明。
因此这个操作直接就变成 \(f_{i,j}\gets f_{i-1,j-1}+v_j\)。
设 \(k=\max f_{i,a_i}\),那么还有一个操作是 \(f_{i,j}\gets k+v_j\),这个操作不好维护,但是我们可以转而维护 \(f_{i,j}-v_j\)。
很显然这个式子也有单调性,相当于不考虑最大值的贡献,此时 \(j\) 越大对前面的限制依然越宽松,这样这个转移就变成了全局 chkmax,也就是前缀赋值操作,前一个转移变成 \(f_{i,j}\gets f_{i-1,j-1}+v_{j-1}\)。
现在我们只要用数据结构维护这个简单 dp 即可。
首先操作二先把所有数循环移位一下,然后打一个懒标记 \(k\) 表示 \(f_{i,j}\) 要加上 \(v_{j-k}\sim v_{j-1}\)。
前缀赋值操作可以用颜色段均摊,动态维护 \(f\) 初值相同的连续段,暴力弹出开头的若干 \(<k\) 的连续段,并且在最后一段上二分分界点即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
#define LF dp.front()
#define RF dp.back()
using namespace std;
const int MAXN=4e5+5;
const ll inf=1e18;
int n,a[MAXN],st[MAXN],v[MAXN],w[MAXN];
ll sv[MAXN];
bool vs[MAXN];
int hd=1,tl=0,tg=0;
struct info {
int len,tg; ll val;
};
deque <info> dp;
ll qryL(int p=hd) {
if(dp.empty()) return -inf;
return LF.val+sv[p-(tg-LF.tg)]-sv[p];
}
ll qryR() {
if(dp.empty()) return -inf;
return RF.val+sv[tl-(tg-RF.tg)]-sv[tl];
}
void popL() { if(!--LF.len) dp.pop_front(); }
void popR() { if(!--RF.len) dp.pop_back(); }
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) {
cin>>a[i];
if(~a[i]) vs[a[i]]=true;
}
for(int i=1;i<=n;++i) {
cin>>w[i];
if(!vs[i]) st[++tl]=i,v[tl]=w[i];
}
for(int i=tl;i>=0;--i) sv[i]=sv[i+1]+v[i];
ll pr=0; dp.push_back({tl,tg,-inf});
for(int i=1,pmx=0;i<=n;++i) {
if(a[i]==-1) {
ll z=qryL(); ++tg;
if(dp.size()) popR(),dp.push_front({1,tg,z});
int sz=0;
while(dp.size()&&qryL(hd+LF.len-1)<pr) hd+=LF.len,sz+=LF.len,dp.pop_front();
if(dp.size()) {
int l=1,r=LF.len,d=0;
while(l<=r) {
int mid=(l+r)>>1;
if(qryL(hd+mid-1)<pr) d=mid,l=mid+1;
else r=mid-1;
}
sz+=d,hd+=d,LF.len-=d;
}
if(sz) dp.push_front({sz,tg,pr}),hd-=sz;
if(hd<tg) popL(),++hd;
} else if(a[i]>pmx) {
ll mx=pr; pmx=a[i];
for(;hd<=tl&&st[hd]<a[i];++hd) mx=max(mx,qryL()+v[hd]),popL();
pr=(hd>tg?mx+w[a[i]]:-inf);
}
cout<<max(pr,qryR()+v[tl])<<" \n"[i==n];
}
return 0;
}
*C. 测试(test)
题目大意
给定 \(a_1\sim a_n,b_1\sim b_{n-1}\),每个 \(b_i\) 可以选择 \(x\in[0,b_i]\),给 \(a_i\) 加上 \(x\),\(a_{i+1}\) 加上 \(b_i-x\)。
\(q\) 次询问 \([l,r]\),求 \(\min a[l,r]\) 的最大值。
数据范围:\(n,q\le 10^5\)。
思路分析
首先二分答案 \(k\),然后可以看成一个二分图匹配问题,左部点是 \(l\sim r\),右部点是所有 \(a,b\)。
用 Hall 定理判定,显然只要考虑左部点集是一个区间的情况,即对每个子区间 \([x,y]\) 要求 \(k(y-x+1)\ge b_{x-1}+\sum_{i=x}^y a_i+b_i\)。
那么答案就是所有子区间 \([x,y]\) 对应的 \(\left\lfloor\dfrac{A_y+B_y-A_{x-1}-B_{x-2}}{y-x+1}\right\rfloor\) 的最小值,其中 \(A,B\) 是 \(a,b\) 的前缀和。
稍加转化变成求区间中最小的 \(\dfrac{R_y-L_x}{y-x}\),其中 \(l-1\le x<y\le r\)。
看成求 \((y,R_y)\) 到所有 \((x,L_x)\) 的最小斜率,那么动态维护 \((x,L_x)\) 构成的凸壳,查询时二分就能求出每个前缀的答案。
对于一般的情况可以分块,隔 \(\sqrt n\) 放一个关键点,然后维护每个关键点的所有前缀和所有后缀的答案。
此时一个查询只剩下 \(x,y\) 都在散块的点对贡献未考虑,这种直接暴力加入这 \(\mathcal O(\sqrt n)\) 个散块元素,还是用刚才的结构维护答案即可。
时间复杂度 \(\mathcal O(\sqrt n\log n)\),瓶颈在二分凸壳上的切点。
我们尝试优化掉二分,用单调队列维护切点。
这个做法的正确性依赖于决策单调性,即每个点的最优转移点递增。
假设 \(x_2<x_1<y_1<y_2\),且 \(y_1\) 的决策点在 \(x_1\),\(y_2\) 的决策点在 \(x_2\)。
首先 \(f(x_1,y_1)>f(x_2,y_1)\),说明 \([x_2,x_1)\) 范围内的平均数 \(>\) \([x_1,y_1]\) 范围内的平均数。
同理 \(f(x_1,y_2)<f(x_2,y_2)\),说明 \([x_2,x_1)\) 范围内的平均数 \(<\) \([x_1,y_2]\) 范围内的平均数。
那么 \([x_1,y_1]\) 范围内的平均数 \(<\) \([x_1,y_2]\) 范围内的平均数,因此这种情况下 \(f(x,y_2)>f(x,y_1)\),则这样的 \(y_2\) 不可能成为答案。
因此我们直接单调队列维护,虽然不能保证每个 \(y\) 的答案都正确,但能保证每个可能成为答案的 \(y\) 的答案都正确,因此每个前缀的答案都正确。
那么查询的是就不用在凸壳上二分,直接单调队列维护切点即可。
时间复杂度 \(\mathcal O(n\sqrt n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
#define LL __int128
using namespace std;
const int MAXN=1e5+5;
const ll inf=1e18;
int n,m;
ll a[MAXN],b[MAXN];
struct poi { ll x,y; };
bool chk(poi u,poi v,poi q) { //slope(u,q)<slope(v,q)
return (LL)(q.y-u.y)*(q.x-v.x)<(LL)(q.y-v.y)*(q.x-u.x);
}
poi st[MAXN];
int hd,tl;
void ins(poi o) {
while(tl>hd&&chk(st[tl-1],st[tl],o)) --tl;
st[++tl]=o;
}
ll qry(poi o) {
if(hd>tl) return inf;
while(hd<tl&&chk(st[hd+1],st[hd],o)) ++hd;
return (o.y-st[hd].y)/(o.x-st[hd].x);
}
const int K=360;
ll f[MAXN/K+5][MAXN];
ll sol(int l,int r) {
if(l%K==0) return f[l/K][r];
if(r%K==0) return f[r/K][l];
if(l/K==r/K) {
ll s=inf; hd=1,tl=0;
for(int i=l;i<=r;++i) s=min(s,qry({i,a[i]})),ins({i,b[i]});
return s;
}
ll s=min(f[l/K+1][r],f[r/K][l]); hd=1,tl=0;
for(int i=l;i<(l/K+1)*K;++i) s=min(s,qry({i,a[i]})),ins({i,b[i]});
for(int i=r/K*K+1;i<=r;++i) s=min(s,qry({i,a[i]})),ins({i,b[i]});
return s;
}
vector<int> testset(vector<int>A,vector<int>B,vector<int>QL,vector<int>QR) {
n=A.size(),m=QL.size();
for(int i=1;i<=n;++i) {
a[i]=a[i-1]+A[i-1]+(i<n?B[i-1]:0);
b[i]=b[i-1]+A[i-1]+(i>1?B[i-2]:0);
}
for(int o=0,x;o*K<=n;++o) {
ll *dp=f[o]; dp[x=o*K]=inf;
hd=1,tl=0,ins({x,b[x]});
for(int i=x+1;i<=n;++i) dp[i]=min(dp[i-1],qry({i,a[i]})),ins({i,b[i]});
hd=1,tl=0,ins({-x,-a[x]});
for(int i=x-1;i>=0;--i) dp[i]=min(dp[i+1],qry({-i,-b[i]})),ins({-i,-a[i]});
}
vector <int> ans;
for(int i=0;i<m;++i) ans.push_back(sol(QL[i],QR[i]+1));
return ans;
}

浙公网安备 33010602011771号