2026 NOI 做题记录(十二)
\(\text{By DaiRuiChen007}\)
A. [P12074] The arithmetic exercise (3)
考虑如何判定一组 \(a_i\) 的符号序列合法,从后往前维护,把 \(j\) 相同的 \(i\) 看成链,插入一个元素的时候只和当前链长奇偶性有关,那么只要记录有多少长度为奇数的链即可。
那么 \(f_{i,j}\) 表示 \([i,n]\) 有多少长度为奇数的链,把状态改写成后缀中有几个符号为 \(+1\) 的元素,可以证明 dp 数组有凸性,转移用 std::multiset 维护,动态计算状态的上下界即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5;
const ll inf=1e18;
ll a[MAXN];
void solve() {
int n,m;
cin>>n>>m;
for(int i=1;i<=m;++i) cin>>a[i];
reverse(a+1,a+m+1);
ll h=0;
multiset<ll> f;
for(int i=1,t,r=0;i<=m;++i,r=t) {
t=min(i,(n+i)/2),f.insert(2*a[i]);
if(t==r) f.erase(f.begin());
if(i&1) h+=*f.rbegin(),f.erase(--f.end());
}
for(auto i:f) h+=max(0ll,i);
cout<<h-accumulate(a+1,a+m+1,0ll)<<"\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
*B. [P13786] LCS of Permutations (8)
首先假设 \(p=[1,\dots,n]\),则 \(a=\mathrm{LIS}(q),b=\mathrm{LIS}(r)\)。
通过 \(a=1,c=n\) 的部分分可以猜测合法条件为 \(abc\ge n,a+b\le c+n\) 及其轮换形式。
首先 \(a+b\le c+n\) 较显然,因为长度为 \(a,b\) 的 \(\mathrm{LIS}\) 至少有长为 \(a+b-n\) 的 \(\mathrm{LCS}\)。
然后考虑 \(abc<n\) 时的情况,首先 \(\mathrm{LDS}(q)\ge \dfrac na\ge bc+1\),考虑 \(\mathrm{LDS}(q)\) 中元素在 \(r\) 中出现的顺序 \(t\), \(\mathrm{LIS}(t)\le \mathrm{LCS}(q,r)=c\),其次 \(\mathrm{LDS}(t)\le \mathrm{LIS}(r)=b\),那么 \(\mathrm{LIS}(t)\times\mathrm{LDS }(t)< |t|\),这显然是不可能的。
考虑 \(n=abc\) 时的构造,考虑在 \(\mathrm{LIS},\mathrm{LDS}\) 一定时 \(n\) 尽可能大的经典构造:若干值域递减的上升序列和若干值域递增的下降序列。
那么直接运用之,\(q\) 为 \(bc\) 个长度为 \(a\) 的上升序列,\(r\) 为 \(b\) 个长度为 \(ac\) 的下降序列,则 \(r\circ q^{-1}\) 为 \(c\) 个值域递增的长度为 \(b\) 的下降序列值域递减地重复 \(a\) 次,此时 \(\mathrm{LCS}(q,r)=a\) 合法。
注意到我们只要取出三个排列中值为 \([1,2,\dots,a],[1,ac+1,2ac+1,\dots,(b-1)ac+1],[1,a+1,2a+1,\dots,(c-1)a+1]\) 的元素构成的子序列(即 \(p,q,r\) 之间的 \(\mathrm{LCS}\) 中元素),此时得到 \(a+b+c-1\) 个元素的排列,如果 \(n>a+b+c-1\),随便选一些剩下的元素放进去即可。
那么只要考虑 \(n\le a+b+c-2\) 的情况,如果 \((a-1)(b-1)(c-1)\ge n-1\),则求 \((n-1,a-1,b-1,c-1)\) 的解然后在末尾插入 \(n\) 即可。
否则 \((a-1)(b-1)(c-1)<n-1\),容易发现此时必有 \(a-1=b-1=1\),即 \(a=b=2,c=n-1\),在 \(q,r=[n,n-1,\dots,1]\) 的基础上交换 \((q_1,q_2),(r_1,r_3)\) 即可。
那么我们就给出了所有 \((n,a,b,c)\) 的构造方法。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
void solve() {
ll n,a,b,c,ty,m;
cin>>n>>a>>b>>c>>ty,m=n;
if(a*b*c<n||a+b>c+n||a+c>b+n||b+c>a+n) return cout<<"No\n",void();
cout<<"Yes\n";
if(!ty) return ;
int k=0;
for(;n&&(a-1)*(b-1)*(c-1)>=n-1;++k,--a,--b,--c,--n);
basic_string <int> B,C;
if(!n);
else if(a+b+c-2>n) {
for(int i=n-1;~i;--i) B+=i,C+=i;
swap(B[0],B[1]),swap(C[1],C[2]);
} else {
map <ll,int> X;
vector <array<ll,2>> Y,Z;
auto ins=[&](ll x) { X[x]=0,Y.push_back({-(x/a),x}),Z.push_back({x/a/c,-x}); };
ins(0);
for(int i=1;i<a;++i) ins(i);
for(int i=1;i<b;++i) ins(a*c*i);
for(int i=1;i<c;++i) ins(a*i);
for(int i=0;i<n&&(int)X.size()<n;++i) if(!X.count(i)) ins(i);
sort(Y.begin(),Y.end()),sort(Z.begin(),Z.end());
int t=0;
for(auto &o:X) o.second=t++;
for(auto o:Y) B+=X[o[1]];
for(auto o:Z) C+=X[-o[1]];
}
for(int i=n;i<m;++i) B+=i,C+=i;
for(int i=0;i<m;++i) cout<<i+1<<" \n"[i==m-1];
for(int i=0;i<m;++i) cout<<B[i]+1<<" \n"[i==m-1];
for(int i=0;i<m;++i) cout<<C[i]+1<<" \n"[i==m-1];
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
C. [P14470] 松鼠 (3)
首先转移就是枚举一种颜色 \(c\),然后把删掉该颜色后形成的每个连续段的 SG 值异或起来。
那么我们可以发现转移一次后得到的区间 \([l,r]\) 一定满足 \(a_{r+1}\) 或 \(a_{l-1}\) 不在 \(a[l,r]\) 中出现过,那么枚举 \(l\) 之后 \(a_{r+1}\not\in a[l,r]\) 的区间只有 \(\le V\) 个。
直接预处理出所有这样的区间的 SG 值,实现时可以按左端点降序处理,为了快速求每种颜色删除后的异或和,我们要快速处理 \(a_{l-1}=a_{r+1}\) 的若干连续区间的 SG 值异或和,这是容易的。
时间复杂度 \(\mathcal O((nV+q)V)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int n,q,a[MAXN],nx[MAXN];
array<int,32> L[MAXN],R[MAXN],f[MAXN],g[MAXN],su[MAXN];
bool vis[MAXN];
int dp(int l,int r) {
if(l==r) return 1;
if(r-l==1) return a[l]==a[r];
unsigned long long s=-1;
for(int v=0;v<32;++v) if(R[l][v]<r) {
int x=R[l][v],y=L[r][v],z=su[x][v]^su[y][v];
if(v!=a[l]||x>l+1) z^=f[l+(v==a[l])][v];
if(v!=a[r]||y<r-1) z^=g[r-(v==a[r])][v];
s&=~(1ll<<z);
}
if(R[l][a[l]]>r) s&=~(1ll<<g[r][a[l]]);
if(R[l][a[r]]==r) s&=~(1ll<<f[l+(a[l]==a[r])][a[r]]);
return __builtin_ctzll(s);
}
vector <array<int,2>> qy[MAXN];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>q;
for(int i=1;i<=n;++i) cin>>a[i],--a[i],L[i+1]=L[i],L[i+1][a[i]]=i;
R[n].fill(n+1);
for(int i=n;i>=1;--i) R[i-1]=R[i],nx[i]=R[i][a[i]],R[i-1][a[i]]=i;
for(int i=1;i<=n;++i) for(int j=0;j<32;++j) if(j!=a[i]) {
qy[L[i][j]+1].push_back({i,j});
}
for(int i=n;i>=1;--i) {
for(int j=0;j<32;++j) qy[i].push_back({R[i][j]-1,32+j});
sort(qy[i].begin(),qy[i].end());
for(auto o:qy[i]) {
if(o[1]<32) g[o[0]][o[1]]=dp(i,o[0]);
else f[i][o[1]-32]=dp(i,o[0]);
}
su[i]=su[i+1];
if(R[i][a[i]]>i+1) su[i][a[i]]^=f[i+1][a[i]];
}
for(int l,r;q--;) cin>>l>>r,cout<<(dp(l,r)?"Toni\n":"Jakov\n");
return 0;
}
*D. [P12204] 委员选举 (7)
把边反向,变成白点的前驱至少有一个黑点。
首先每个 SCC 内部可以直接黑白间隔染色,但我们要使得 SCC 之间没有黑点向黑点的连边。
考虑按拓扑序处理,每次把所有没有入度的 SCC 按上述方法染色,注意到此时每个黑点的所有后继条件已满足且必定染白,则可以删掉这些点。
递归该过程就能得到构造,考虑优化复杂度,注意到删掉只会把原本的 SCC 进一步分裂,因此对于原本的一个 SCC,删掉若干个点后该图依旧是二分图,但此时直接黑白染色可能会让一些没有入度的点被染白。
那么把没有入度的点染黑,然后递归地删除后继,用类似拓扑排序的方法删点直到所有点有入度,然后取一组黑白染色即可。
时间复杂度 \(\mathcal O(n+m)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
basic_string <int> G[MAXN],V[MAXN],E[MAXN];
int n,m,dfn[MAXN],low[MAXN],dcnt,st[MAXN],tp,bl[MAXN],scnt,deg[MAXN],in[MAXN];
bool ins[MAXN],vis[MAXN],col[MAXN];
void dfs1(int u) {
dfn[u]=low[u]=++dcnt,st[++tp]=u,ins[u]=true;
for(int v:G[u]) {
if(!dfn[v]) dfs1(v),low[u]=min(low[u],low[v]);
else if(ins[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]) for(++scnt;ins[u];ins[st[tp--]]=false) bl[st[tp]]=scnt;
}
void dfs2(int u,int x) {
vis[u]=true;
for(int v:G[u]) if(bl[v]==x&&!vis[v]) col[v]=col[u]^1,dfs2(v,x);
}
void solve(int x) {
queue <int> Q;
for(int u:V[x]) if(!vis[u]&&!in[u]) Q.push(u);
while(Q.size()) {
int u=Q.front(); Q.pop(),col[u]=1,vis[u]=1;
for(int v:G[u]) if(bl[v]==x&&!vis[v]) {
vis[v]=1;
for(int e:G[v]) if(bl[e]==x&&!vis[e]&&!--in[e]) Q.push(e);
}
}
for(int u:V[x]) if(!vis[u]) dfs2(u,x);
for(int u:V[x]) if(col[u]) for(int v:G[u]) if(!vis[v]) {
vis[v]=1;
for(int e:G[v]) if(bl[e]==bl[v]) --in[e];
}
}
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,G[v]+=u;
for(int i=1;i<=n;++i) if(!dfn[i]) dfs1(i);
for(int i=1;i<=n;++i) {
V[bl[i]]+=i,dfn[i]=low[i]=0;
for(int j:G[i]) {
if(bl[i]^bl[j]) E[bl[i]]+=bl[j],++deg[bl[j]];
else ++in[j];
}
}
dcnt=0;
queue <int> Q;
for(int i=1;i<=scnt;++i) if(!deg[i]) Q.push(i);
while(Q.size()) {
int u=Q.front(); Q.pop(),solve(u);
for(int v:E[u]) if(!--deg[v]) Q.push(v);
}
cout<<count(col+1,col+n+1,1)<<"\n";
for(int i=1;i<=n;++i) if(col[i]) cout<<i<<" ";
cout<<"\n";
return 0;
}
E. [P11924] 贪婪大盗 (4.5)
首先从后往前 dp,动态维护当前后缀中每个人获得的钱数 \(f_i\),那么加入一个新的人 \(x\) 就会按 \(f_i+a_i\) 从小到大满足一半人的需求,而剩下的人 \(f\gets 0\),如果 \(m\) 不够分则 \(f_x\gets -\infty\)。
注意到 \(f\) 值域是 \(\mathcal O(V)\) 的,因为前一个 \(f_i\ne\infty\) 的元素执行后至多一半的 \(f_i>0\),则当前时刻至少有一半 \(f_i\le 0\),直接满足这些人的要求,此时每个人至多支付 \(a_i\) 的金币。
所以可以直接维护每种 \((a_i,f_i)\) 中有多少个元素从而算出被满足的点应该有 \(a_i+f_i\le w\),只有 \(\le 1\) 个人可能有 \(f_i>V\),可以直接维护。
但我们要对 \(a_i+f_i=w\) 的点满足标号较大的若干个人的要求,所以需要用一些数据结构维护 \((a_i,f_i)\) 的界。
平衡树不好同时对 \(\mathcal O(V)\) 棵平衡树一起二分,因此考虑用线段树合并和线段树分裂代替之。
时间复杂度 \(\mathcal O(nV^2+nV\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e4+5,MAXS=4e7+5;
int n,m,ls[MAXS],rs[MAXS],ct[MAXS],tot;
void ins(int x,int &p,int l=1,int r=n) {
if(!p) p=++tot;
++ct[p];
if(l==r) return ;
int mid=(l+r)>>1;
x<=mid?ins(x,ls[p],l,mid):ins(x,rs[p],mid+1,r);
}
void merge(int q,int &p,int l=1,int r=n) {
if(!p||!q) return p|=q,void();
ct[p]+=ct[q];
if(l==r) return ;
int mid=(l+r)>>1;
merge(ls[q],ls[p],l,mid),merge(rs[q],rs[p],mid+1,r);
}
void split(int u,int x,int &p,int &q,int l=1,int r=n) {
if(!u) return p=q=0,void();
if(l==r) return p=u,q=0,void();
int mid=(l+r)>>1;
if(x<=mid) p=++tot,q=u,split(ls[u],x,ls[p],ls[q],l,mid);
else p=u,q=++tot,split(rs[u],x,rs[p],rs[q],mid+1,r);
ct[q]=ct[ls[q]]+ct[rs[q]],ct[p]=ct[ls[p]]+ct[rs[p]];
}
int q=64,a[MAXN],f[MAXN],rt[75][75],b[75],e[75],nw[75][75];
void dfs(int x,int p,int l=1,int r=n) {
if(!ct[p]) return ;
if(l==r) return f[l]=x,void();
int mid=(l+r)>>1;
dfs(x,ls[p],l,mid),dfs(x,rs[p],mid+1,r);
}
int qry(int k) {
int l=1,r=n;
for(memcpy(e,b,sizeof(e));l<r;) {
int mid=(l+r)>>1,w=0;
for(int i=1;i<=q;++i) w+=ct[ls[e[i]]];
for(int i=1;i<=q;++i) e[i]=(w<k?rs[e[i]]:ls[e[i]]);
if(w<k) k-=w,l=mid+1;
else r=mid;
}
return l;
}
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];
reverse(a+1,a+n+1);
int o=0;
for(int k=1;k<=n;++k) {
memset(nw,0,sizeof(nw));
int t=(k-1)/2; f[k]=m;
for(int v=0;;++v) {
int c=0;
for(int i=1;i<=q;++i) b[i]=(!v?rt[q+1][i]:(i<=v?rt[v-i][i]:0)),c+=ct[b[i]];
f[k]-=v*min(c,t);
if(f[k]<0) break;
if(c>=t) {
int h=qry(t);
for(int i=1,l,r;i<=q;++i) {
split(b[i],h,l,r),merge(l,nw[v][i]),merge(r,nw[0][i]);
}
for(int y=1;y<=q;++y) merge(rt[q+1][y],nw[0][y]);
for(int x=0;x<=q;++x) for(int y=1;y<=q;++y) {
if(x+y>v) merge(rt[x][y],nw[0][y]);
if(x+y<v) merge(rt[x][y],nw[x+y][y]);
}
break;
}
t-=c;
}
if(f[k]<0) { f[k]=-1,ins(k,rt[q+1][a[k]]); continue; }
if(o) ins(o,nw[0][a[o]]),o=0;
if(f[k]<=q) ins(k,nw[f[k]][a[k]]);
else o=k;
memcpy(rt,nw,sizeof(nw));
}
for(int x=0;x<=q+1;++x) for(int y=1;y<=q;++y) dfs(x>q?-1:x,rt[x][y]);
for(int i=n;i;--i) cout<<f[i]<<" \n"[i==1];
return 0;
}
*F. [P14471] 火花 (8)
首先 \(t=0\) 时是经典后序背包,在 dfn 序上 dp 即可做到 \(\mathcal O(nk)\)。
考虑如何在 dfn 序上背包时处理子树信息,注意到我们的限制是二操作上 \(u\) 不能选超过 \(c_u-w_u\) 个,其中 \(w_u\) 为 \(u\) 子树内有多少个被操作一选择的点。
把子树信息看成 dfn 序上的区间信息,那么 \(s_{l_u},s_{r_u}\) 表示 dfn 序中 \(u\) 子树的开始和结束位置处,对应的前缀中操作一共选了多少个点,则 \(w_u=s_{r_u}-s_{l_u}\)。
那么操作二选择次数上界 \(c_u-w_u=c_u-s_{r_u}+s_{l_u}\),注意到 \(c_u-s_{r_u}\ge c_u-t>0\),所以看成在 \(l_u\) 处选 \([0,s_{l_u}]\) 个并在 \(r_u\) 处选 \([1,c_u-s_{r_u}]\) 个即可。
那么 dp 的时候只要记录前缀中操作一和二分别选了多少个,都可以用单调队列优化转移。
注意如果 \(u\) 未被操作二选择,则直接从 \(l_u\to r_u\) 转移,此时要考虑 \(u\) 子树内操作一选择的点,显然是深度最大的若干个,由于该函数具有凸性,因此可以决策单调性优化。
时间复杂度 \(\mathcal O(nkt\log t)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e4+5;
const ll inf=1e18;
int n,k,t,b[MAXN],fa[MAXN],a[MAXN*2],m,in[MAXN],ot[MAXN];
ll w[MAXN],d[MAXN];
vector<int> G[MAXN];
basic_string<ll> c[MAXN];
void dfs(int u) {
d[u]=w[u]+d[fa[u]],c[u]={d[u]},a[++m]=u,in[u]=m;
for(int v:G[u]) {
dfs(v);
int o=c[u].size(); c[u]+=c[v];
inplace_merge(c[u].begin(),c[u].begin()+o,c[u].end(),greater<>());
if((int)c[u].size()>t) c[u].resize(t);
}
a[++m]=u,ot[u]=m;
}
vector<vector<ll>>f[MAXN*2];
ll g[MAXN],h[MAXN];
void cdq(int l,int r,int L,int R,const basic_string<ll>&C) {
if(l>r) return ;
int mid=(l+r)>>1,M=0;
for(int i=L;i<=R&&i<=mid;++i) {
ll z=g[mid-i]+C[i];
if(z>h[mid]) h[mid]=z,M=i;
}
cdq(l,mid-1,L,M,C),cdq(mid+1,r,M,R,C);
}
void trs(int L,int R) {
static int q[MAXN];
for(int i=0,l=1,r=0;i<=k;++i) {
for(;l<=r&&q[l]<i-R;++l);
h[i]=L?-inf:g[i];
if(l<=r) h[i]=max(h[i],g[q[l]]);
for(;l<=r&&g[q[r]]<=g[i];--r);
q[++r]=i;
}
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>k>>t;
for(int i=1;i<=n;++i) cin>>b[i]>>w[i];
for(int i=2;i<=n;++i) cin>>fa[i],G[fa[i]].push_back(i);
dfs(1);
for(int i=1;i<=n;++i) {
c[i].insert(c[i].begin(),0);
for(int j=1;j<(int)c[i].size();++j) c[i][j]+=c[i][j-1];
}
for(int i=0;i<=2*n;++i) f[i]=vector<vector<ll>>(t+1,vector<ll>(k+1,-inf));
f[0][0][0]=0;
for(int o=1;o<=2*n;++o) {
int u=a[o];
if(o==in[u]) {
for(int j=0;j<=k;++j) {
for(int i=0;i<=t;++i) g[i]=f[o-1][i][j],h[i]=-inf;
cdq(0,t,0,c[u].size()-1,c[u]);
for(int i=0;i<=t;++i) f[ot[u]][i][j]=max(f[ot[u]][i][j],h[i]);
}
for(int i=0;i<=t;++i) {
for(int j=0;j<=k;++j) g[j]=f[o-1][i][j]-w[u]*j;
trs(0,i);
for(int j=0;j<=k;++j) {
f[o][i][j]=max(f[o][i][j],h[j]+w[u]*j);
if(i<t) f[o][i+1][j]=max(f[o][i+1][j],h[j]+w[u]*j+d[u]);
}
}
} else {
for(int i=0;i<=t;++i) {
for(int j=0;j<=k;++j) g[j]=f[o-1][i][j]-w[u]*j;
trs(1,b[u]-i);
for(int j=0;j<=k;++j) f[o][i][j]=max(f[o][i][j],h[j]+w[u]*j);
}
}
}
cout<<*max_element(f[2*n][t].begin(),f[2*n][t].end())<<"\n";
return 0;
}
G. [P11611] 归约 (3.5)
限制看成若干 \(s[l,r]\ne t\)。
考虑容斥,首先把互相包含且可以共存的限制删除,可以 AC 自动机描述,即任意 \(s[l,i]\) 不能经过 \(r=i\) 的所有串在 Fail 树上的子树,因此可以单 \(\log\) 处理。
此时我们直接容斥,钦定若干限制被满足,由于我们进行了简化操作,此时选择的区间左右端点必定递增,直接按 \(r\) 排序 dp。
只要考虑相邻区间的关系,如果上一个区间和当前区间有交,枚举对应的后缀和前缀,可以用字符串哈希快速转移,如果无交则树状数组简单维护。
时间复杂度 \(\mathcal O(|S|\log |S|)\)。
更优的做法是看成在 \((i,0/1)\) 游走的路径,要求路径不能包含某些子串,对这些子串建立 AC 自动机变成 DAG 路径计数,分析图的性质可以做到 \(\mathcal O(n+|S|)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
using namespace std;
const int MAXN=1e6+5,MOD=1e9+7,i2=(MOD+1)/2,P=1e6+3;
ll pw[MAXN],ipw[MAXN],f[MAXN];
int n,m,tr[MAXN][2],fa[MAXN],tot;
struct info { int l,r,p; vector<int>w; } a[MAXN];
int ins(vector<int>&s) {
int p=0;
for(int c:s) p=tr[p][c]=tr[p][c]?tr[p][c]:++tot;
return p;
}
vector <int> G[MAXN];
int dfn[MAXN],efn[MAXN],dcnt;
void build() {
queue <int> Q;
for(int c:{0,1}) if(tr[0][c]) Q.push(tr[0][c]);
while(Q.size()) {
int u=Q.front(); Q.pop(),G[fa[u]].push_back(u);
for(int c:{0,1}) {
if(tr[u][c]) fa[tr[u][c]]=tr[fa[u]][c],Q.push(tr[u][c]);
else tr[u][c]=tr[fa[u]][c];
}
}
}
void dfs(int u) { dfn[u]=++dcnt; for(int v:G[u]) dfs(v); efn[u]=dcnt; }
struct BIT {
ll tr[MAXN],s;
void add(int x,int v) { for(;x<=n+1;x+=x&-x) tr[x]=(tr[x]+v)%MOD; }
ll qry(int x) { for(s=0;x;x&=x-1) s=(s+tr[x])%MOD; return s; }
} T;
struct BIT2 {
int tr[MAXN],s,st[MAXN],tp;
void add(int x,int v) { for(st[++tp]=x;x<=dcnt;x+=x&-x) tr[x]+=v; }
int qry(int x) { for(s=0;x;x&=x-1) s+=tr[x]; return s; }
void init() { while(tp) for(int x=st[tp--];x<=dcnt;x+=x&-x) tr[x]=0; }
} Q;
vector <array<int,2>> ad[MAXN];
mt19937_64 rnd(time(0));
ull hv[MAXN][2],ch[MAXN];
int ct,nx[MAXN],hd[P],g[MAXN];
void add(ull s,int z) {
for(int x=hd[s%P];x;x=nx[x]) if(ch[x]==s) return g[x]=(g[x]+z)%MOD,void();
ch[++ct]=s,g[ct]=z,nx[ct]=hd[s%P],hd[s%P]=ct;
}
int qry(ull s) {
for(int x=hd[s%P];x;x=nx[x]) if(ch[x]==s) return g[x];
return 0;
}
signed main() {
cin>>n,getchar(); string S;
for(int i=pw[0]=ipw[0]=1;i<=n+1;++i) pw[i]=pw[i-1]*2%MOD,ipw[i]=ipw[i-1]*i2%MOD,hv[i][0]=rnd(),hv[i][1]=rnd();
for(char o=getchar();o!='\n';o=getchar()) if(o!=' ') S+=o;
for(int l=0,r;l<(int)S.size();l=r+2) {
for(r=l;S[r]!=')';++r);
int L=n,R=0;
vector <int> s;
for(int x=l+1,y,z;x<r;x=y+1) {
for(y=x+1+(S[x]=='~'),z=0;isdigit(S[y]);++y) z=z*10+S[y]-'0';
s.push_back(S[x]=='~'?-z:z),L=min(L,z),R=max(R,z);
}
++m,a[m].l=L,a[m].r=R,a[m].w=vector<int>(R-L+1,1);
for(int x:s) if(x>0) a[m].w[x-L]=0;
a[m].p=ins(a[m].w);
}
build(),dfs(0);
for(int i=1;i<=m;++i) {
ad[a[i].r].push_back({dfn[a[i].p],1}),ad[a[i].r].push_back({efn[a[i].p]+1,-1});
}
for(int i=1;i<=n;++i) {
ad[i].push_back({0,0}),sort(ad[i].begin(),ad[i].end());
for(int j=1;j<(int)ad[i].size();++j) ad[i][j][1]+=ad[i][j-1][1];
}
sort(a+1,a+m+1,[&](auto&x,auto&y){ return x.r^y.r?x.r<y.r:x.l>y.l; });
T.add(1,ipw[1]);
for(int i=1;i<=m;++i) {
if(i>1&&a[i].r>a[i-1].r) Q.init();
if(Q.qry(dfn[a[i].p])) continue;
bool ban=0;
for(int j=a[i].l,p=0;j<a[i].r;++j) {
p=tr[p][a[i].w[j-a[i].l]];
if((*--lower_bound(ad[j].begin(),ad[j].end(),array<int,2>{dfn[p]+1,-m}))[1]) { ban=1; break; }
}
if(ban) continue;
Q.add(dfn[a[i].p],1),Q.add(efn[a[i].p]+1,-1);
f[i]=pw[a[i].l]*T.qry(a[i].l)%MOD;
ull h=0;
for(int j=a[i].l;j<a[i].r;++j) {
h+=hv[j][a[i].w[j-a[i].l]],f[i]=(f[i]+qry(h))%MOD;
}
h=0,f[i]=MOD-f[i],T.add(a[i].r+1,f[i]*ipw[a[i].r+1]%MOD);
for(int j=a[i].r;j>a[i].l;--j) {
h+=hv[j][a[i].w[j-a[i].l]],add(h,f[i]);
}
}
cout<<pw[n+1]*T.qry(n+1)%MOD<<"\n";
return 0;
}
*H. [P11834] 岁月 (7.5)
首先 \(w=1\) 考虑的部分,要求缩点后的 DAG 只有一个入度为 \(0\) 的 SCC。
回顾一下经典的主旋律容斥,我们加入一层入度为 \(0\) 的点 \(T\) 系数为 \((-1)^{|T|-1}w_T\)。
考虑对真实的入度为 \(0\) 的点集 \(S\) 做容斥,此时 \(w_T\) 表示 \(T\subseteq S\) 的贡献和,则容斥得到 \(\sum_{S\ne\varnothing}\sum_{S\subseteq T} (-1)^{|T|-|S|} w_T=\sum (-1)^{|T|-1}w_T\)。
那么我们主旋律容斥算出 \(S\) 形成 SCC 的方案数,\(S\) 中划分出奇数或偶数个 SCC 的方案数,以及 \(S\) 中点连成 DAG 的方案数。
然后计算答案的时候枚举根所在的 SCC,同样用主旋律容斥计算出无入度点集恰好为该 SCC 的 DAG 数量,可以做到 \(\mathcal O(3^n)\)。
加上 \(w\) 的限制,那么每次我们会把上一层得到的若干连通块之间连上边。
考虑如何记录状态,我们要对同时状压选出哪些连通块,以及连通块内部的一定信息。
首先上一层的每个连通块都要有外向生成树,那么我们在当前层处理时记录考虑到的连通块的集合,以及其中的每个连通块的根集合,这样的状态数依旧是 \(\mathcal O(2^n)\) 的,转移类似,复杂度可以接受。
注意要特殊处理形态不改变的连通块。
时间复杂度 \(\mathcal O(n^22^n+3^n)\)。
代码:
#include<bits/stdc++.h>
#define clr(a) memset(a,0,sizeof(a))
using namespace std;
const int MOD=1e9+7,i2=(MOD+1)/2;
int n,m,ct[1<<15],dp[1<<15],f[1<<15],g[1<<15][2],h[1<<15],pw[255],ipw[255];
int E(int s,int t) { return ct[s|t]-ct[s-(s&t)]-ct[t]+2*ct[s&t]; }
vector <array<int,2>> G[255];
int dsu[15],bl[15],st[1<<15],in[1<<15],pr[1<<15],sum[1<<15];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
void solve() {
cin>>n>>m,clr(h);
for(int i=1;i<=m;++i) G[i].clear();
for(int i=1,u,v,w;i<=m;++i) cin>>u>>v>>w,G[w].push_back({u-1,v-1});
for(int i=0;i<n;++i) dsu[i]=i,h[1<<i]=1;
for(int w=1;w<=m;++w) {
clr(st),clr(ct);
for(int i=0;i<n;++i) bl[i]=find(i),st[1<<bl[i]]|=1<<i;
for(int s=0;s<(1<<n);++s) st[s]=st[s&-s]|st[s&(s-1)];
for(auto e:G[w]) {
for(int s=0;s<(1<<n);++s) if((s>>e[0]&1)&&(s>>e[1]&1)) ++ct[s];
if(find(e[0])!=find(e[1])) dsu[find(e[0])]=find(e[1]);
}
for(int rt=0;rt<n;++rt) if(dsu[rt]==rt) {
int U=0;
for(int i=0;i<n;++i) if(bl[i]==i&&find(i)==rt) U|=st[1<<i];
if(U==st[1<<rt]) {
for(int s=1;s<(1<<n);++s) if((s&U)==s) h[s]=1ll*h[s]*pw[E(U,U)]%MOD;
continue;
}
clr(dp),clr(f),clr(g),clr(in),clr(pr),clr(sum);
for(int s=0;s<(1<<n);++s) if((s&U)==s) {
for(int i=0;i<n;++i) if(s>>i&1) in[s]|=1<<bl[i];
}
dp[0]=f[0]=g[0][0]=1;
for(int s=1;s<(1<<n);++s) if((s&U)==s) {
int S=in[s];
for(int T=S,t;T;T=(T-1)&S) if(T&(S&-S)) {
t=s&st[in[T]];
g[s][0]=(g[s][0]+1ll*f[t]*g[s^t][1])%MOD;
g[s][1]=(g[s][1]+1ll*f[t]*g[s^t][0])%MOD;
}
f[s]=pw[E(st[S],s)];
for(int T=S,t;T;T=(T-1)&S) {
t=s&st[T];
f[s]=(f[s]+1ll*(g[t][0]+MOD-g[t][1])*pw[E(st[S],s^t)])%MOD;
}
g[s][1]=(g[s][1]+f[s])%MOD;
for(int T=S,t;T;T=(T-1)&S) {
t=s&st[T];
dp[s]=(dp[s]+1ll*(g[t][1]+MOD-g[t][0])*pw[E(st[T],s^t)]%MOD*dp[s^t])%MOD;
}
}
for(int s=0,S;s<(1<<n);++s) if((s&U)==s) {
S=in[s],pr[s]=1;
for(int i=0;i<n;++i) if(S>>i&1) pr[s]=1ll*pr[s]*h[s&st[1<<i]]%MOD;
sum[S]=(sum[S]+1ll*pr[s]*dp[s]%MOD*pw[E(U^st[S],s)+E(U,st[S]^s)])%MOD;
}
for(int s=1;s<(1<<n);++s) if((s&U)==s) {
h[s]=0;
for(int o=U^st[in[s]],t=o;;t=(t-1)&o) {
h[s]=(h[s]+1ll*pr[s|t]*(g[t][0]+MOD-g[t][1])%MOD*sum[in[U]^in[s|t]]%MOD*pw[E(U,st[in[s|t]]^s^t)])%MOD;
if(!t) break;
}
h[s]=1ll*h[s]*f[s]%MOD;
}
}
}
int ans=0;
for(int s=1;s<(1<<n);++s) ans=(ans+h[s])%MOD;
cout<<1ll*ans*ipw[2*m]%MOD<<"\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
for(int i=pw[0]=ipw[0]=1;i<255;++i) pw[i]=pw[i-1]*2%MOD,ipw[i]=1ll*ipw[i-1]*i2%MOD;
int ty,_; cin>>ty>>_;
while(_--) solve();
return 0;
}
I. [P11880] 选区间 (4)
首先二分答案 \(k\),然后枚举二操作个数 \(t\),看成给在全选一操作的基础上给若干区间 \(-2\) 使得最大值 \(\le k-t\)。
可以用堆维护贪心,每次弹出右端点最大的线段,复杂度 \(\mathcal O(n^2\log^2n)\)。
观察二操作的性质,首先注意到如果选了两个不相交的区间,那么都不选肯定更优。
进一步,对于两个有交的区间,同时不选只会让它们的交覆盖次数 \(-2\)。
设 \(w\) 表示初始覆盖次数的最大值,那么 \(t\) 不会超过 \(w-k+1\),否则删掉左端点最大和右端点最小的区间,则区间交部分覆盖次数依旧 \(\ge w-k\),其他位置覆盖次数不降,依旧合法。
那么 \(t\) 只有 \(\mathcal O(1)\) 个取值。
时间复杂度 \(\mathcal O(n\log^2n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=4e5+5;
vector <array<int,2>> R[MAXN];
int n,m,mx,a[MAXN],f[MAXN],o[MAXN];
bool sol(int up,int t) {
memset(f,0,sizeof(f)),memset(o,0,sizeof(o)),up-=t;
priority_queue <array<int,2>> Q;
for(int i=0,z=0;i<=m;++i) {
for(auto x:R[i]) Q.push(x);
for(z+=f[i];a[i]-z>up;Q.pop(),--t) {
if(Q.empty()||!t||Q.top()[0]<i) return false;
z+=2,f[Q.top()[0]+1]-=2,o[Q.top()[1]]=1;
}
}
return true;
}
bool chk(int k) { return sol(k,mx-k)||sol(k,mx-k+1); }
signed main() {
cin>>n,m=2*n;
for(int i=1,l,r;i<=n;++i) cin>>l>>r,++a[l],--a[r+1],R[l].push_back({r,i});
for(int i=1;i<=m;++i) mx=max(mx,a[i]+=a[i-1]);
int z=mx;
for(int k=1<<__lg(z);k;k>>=1) if(z>=k&&chk(z-k)) z-=k;
cout<<z<<"\n",chk(z);
for(int i=1;i<=n;++i) cout<<1-o[i]; cout<<"\n";
return 0;
}
*J. [P14355] 封印 (7.5)
首先枚举首个产生贡献的时刻 \(L\) 以及统计贡献的时刻 \(R\)。
分讨所有线段的贡献,如果 \(l\in[L,R]\) 则必须选 \([l,\min(r,R)]\),如果 \(r\le R\) 可以贡献到答案。
如果 \(l<L\le r\le R\) 则可以主动选择是否贡献到答案中。
注意到我们只要判断 \([L,R]\) 中每个位置覆盖次数是否 \(\le k\) 即可,因为 \([1,L-1]\) 的覆盖次数不超过 \(L\) 的覆盖次数。
枚举 \(L,R\) 无法接受,注意到 \(L\) 确定时,\(R\) 应该取到最大值,即保留 \(l \ge L\) 线段时最后一个覆盖次数 \(\le k\) 的位置。
否则 \(R\gets R+1\) 的时候,不存在 \(l<L,r>R\) 的线段,因此不会让 \(l\in [L,R]\) 的线段导出不合法,那么原本的最优解依旧合法,且有更多 \(l<L\) 的线段可以选择,且更多 \(l\in[L,R]\) 的线段产生贡献。
所以降序扫描 \(L\),双指针维护 \(R\),用维护 \(l<L\) 线段的贪心过程,这些线段显然满足拟阵模型,因此到增减 \(L,R\) 只会影响 \(\mathcal O(1)\) 条其中的线段,简单线段树维护调整的过程即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
typedef array<int,2> pii;
const int MAXN=6e5+5,inf=1e9+1,N=1<<20;
int n,k,L[MAXN],R[MAXN],w[MAXN],a[MAXN];
struct Segt1 {
int mx[N<<1],ad[N<<1];
void adt(int p,int z) { mx[p]+=z,ad[p]+=z; }
void psd(int p) { if(ad[p]) adt(p<<1,ad[p]),adt(p<<1|1,ad[p]),ad[p]=0; }
void psu(int p) { mx[p]=max(mx[p<<1],mx[p<<1|1]); }
void add(int ul,int ur,int z,int l=1,int r=2*n,int p=1) {
if(ul<=l&&r<=ur) return adt(p,z);
int mid=(l+r)>>1; psd(p);
if(ul<=mid) add(ul,ur,z,l,mid,p<<1);
if(mid<ur) add(ul,ur,z,mid+1,r,p<<1|1);
psu(p);
}
int nxt(int x,int z,int l=1,int r=2*n,int p=1) {
if(mx[p]<z) return 2*n+1;
if(l==r) return l;
int mid=(l+r)>>1,t=2*n+1; psd(p);
if(x<=mid) t=nxt(x,z,l,mid,p<<1);
return t<=2*n?t:nxt(x,z,mid+1,r,p<<1|1);
}
int pre(int x,int z,int l=1,int r=2*n,int p=1) {
if(mx[p]<z) return 0;
if(l==r) return l;
int mid=(l+r)>>1,t=0; psd(p);
if(x>mid) t=pre(x,z,mid+1,r,p<<1|1);
return t?t:pre(x,z,l,mid,p<<1);
}
} T;
struct Segt2 {
pii tr[N<<1];
Segt2() { fill(tr,tr+(N<<1),pii{inf,0}); }
void upd(int x,int z) {
for(tr[x+N]={z,x},x+=N;x>1;x>>=1) tr[x>>1]=min(tr[x],tr[x^1]);
}
pii qry(int l,int r) {
pii s={inf,0};
for(l+=N-1,r+=N+1;l^r^1;l>>=1,r>>=1) {
if(~l&1) s=min(s,tr[l^1]);
if(r&1) s=min(s,tr[r^1]);
}
return s;
}
} S;
struct Segt3 {
pii tr[N<<1];
void upd(int x,int z) {
for(tr[x+N]={z,x},x+=N;x>1;x>>=1) tr[x>>1]=max(tr[x],tr[x^1]);
}
pii qry(int l,int r) {
pii s={0,0};
for(l+=N-1,r+=N+1;l^r^1;l>>=1,r>>=1) {
if(~l&1) s=min(s,tr[l^1]);
if(r&1) s=min(s,tr[r^1]);
}
return s;
}
} Q;
bool vis[MAXN];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;++i) cin>>L[i]>>R[i]>>w[i],a[L[i]]=a[R[i]]=i;
ll ans=0,sum=0;
for(int l=2*n,r=2*n;l>=1;--l) {
if(l==R[a[l]]) {
int h=T.nxt(l,k);
if(h>l) sum+=w[a[l]],vis[a[l]]=1,T.add(L[a[l]],l,1),S.upd(l,w[a[l]]);
else {
auto o=S.qry(l,r);
if(o[1]&&o[0]<w[a[l]]) {
vis[a[o[1]]]=0,S.upd(o[1],inf),T.add(L[a[o[1]]],o[1],-1),Q.upd(o[1],o[0]);
sum+=w[a[l]]-o[0],vis[a[l]]=1,T.add(L[a[l]],l,1),S.upd(l,w[a[l]]);
} else Q.upd(l,w[a[l]]);
}
} else if(!vis[a[l]]) {
Q.upd(R[a[l]],0),T.add(l,R[a[l]],1),vis[a[l]]=1;
if(R[a[l]]<=r) Q.upd(R[a[l]],0),sum+=w[a[l]];
for(int h=T.pre(r,k+1);h>=l;h=T.pre(r,k+1)) {
auto o=S.qry(h,r);
if(o[1]) {
sum-=o[0],vis[a[o[1]]]=0,S.upd(o[1],inf),T.add(L[a[o[1]]],o[1],-1),Q.upd(o[1],o[0]);
o=Q.qry(l,min(r,T.nxt(l,k)-1));
if(o[0]) sum+=o[0],vis[a[o[1]]]=1,Q.upd(o[1],0),S.upd(o[1],o[0]),T.add(L[a[o[1]]],o[1],1);
} else for(;r>=h;--r) {
if(r==R[a[r]]) {
if(L[a[r]]>=l) sum-=w[a[r]];
else if(!vis[a[r]]) Q.upd(r,0);
else {
sum-=w[a[r]],vis[a[r]]=0,S.upd(r,inf),T.add(L[a[r]],r,-1);
o=Q.qry(l,min(h,T.nxt(l,k))-1);
if(o[0]) sum+=o[0],vis[a[o[1]]]=1,Q.upd(o[1],0),S.upd(o[1],o[0]),T.add(L[a[o[1]]],o[1],1);
}
} else vis[a[r]]=0,T.add(r,R[a[r]],-1);
}
}
} else S.upd(R[a[l]],inf);
ans=max(ans,sum);
}
cout<<ans<<"\n";
return 0;
}
K. [P12558] Heroes and Monsters (4)
设 \(a,b\) 有序枚举 \(k\),策略一定是顺序匹配 \(b_{1}\sim b_k\) 以及 \(b_{k+1}\sim b_n\)。
那么 \(f_{i,j}\) 表示前 \(i\) 个元素选了多少个 \(S\) 中元素,则 \(b_1\sim b_n\) 都会对某个时刻的 \(j\) 有限制。
进一步观察发现 \(b_1\sim b_k\) 都是限制前缀中 \(j\) 的上界为定值,而 \(b_{k+1}\sim b_n\) 都是限制 \(i-j+k\) 的下界为定值,即限制后缀中 \(\not\in S\) 元素的上界。
而这两种元素限制的范围不交,所以预处理出前缀满足 \(b[1,k]\) 限制,后缀满足 \(b[k,n]\) 限制的方案数合并。
时间复杂度 \(\mathcal O(n^2)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5005,MOD=998244353;
int n,m,a[MAXN],b[MAXN],p[MAXN],l[MAXN],r[MAXN],f[MAXN][MAXN],g[MAXN][MAXN],s[MAXN];
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];
for(int i=1;i<=n;++i) cin>>b[i];
sort(a+1,a+n+1),sort(b+1,b+n+1);
for(int i=0;i<=n+1;++i) l[i]=i,r[i]=n+1-i;
for(int i=1;i<=n;++i) {
p[i]=upper_bound(a+1,a+n+1,b[i])-a-1;
l[p[i]]=min(l[p[i]],i-1);
r[p[i]+1]=min(r[p[i]+1],n-i);
}
f[0][0]=g[n+1][0]=1;
for(int i=1;i<=n;++i) {
for(int j=0;j<=l[i];++j) f[i][j]=(f[i-1][j]+(j?f[i-1][j-1]:0))%MOD;
}
for(int i=n;i;--i) {
for(int j=0;j<=r[i];++j) g[i][j]=(g[i+1][j]+(j?g[i+1][j-1]:0))%MOD;
}
s[0]=s[n]=1;
for(int i=1;i<=n;++i) s[0]&=a[i]<b[i],s[n]&=a[i]>b[i];
for(int k=1;k<n;++k) {
for(int i=0;i<=k;++i) {
int j=(n-k)-(p[k]-i);
if(j>=0) s[k]=(s[k]+1ll*f[p[k]][i]*g[p[k]+1][j])%MOD;
}
}
for(int i=1;i<=n;++i) s[i]=(s[i-1]+s[i])%MOD;
cin>>m;
for(int L,R;m--;) cin>>L>>R,cout<<(s[R]+(L?MOD-s[L-1]:0))%MOD<<"\n";
return 0;
}
*L. [P12636] Array Reduction (7)
令 \(k\gets k+t\),首先枚举操作次数 \(c\),要求 \(\sum \max(\lceil\frac{a_i+tc}{k}\rceil,0)\ge c\)。
枚举 \(a_i+tc\ge 0\) 的界 \([1,m]\),左式写成 \(\sum\lceil\frac{a_i+tc}{k}\rceil\),可以拆成 \(\sum \lceil \frac{a_i}k\rceil,m\lfloor\frac {tc}k\rfloor\) 以及 \(\sum[tc\bmod k+(a_i+k-1)\bmod k\ge k]\),最后一部分只要离散化后可以用树状数组维护,因此可以快速校验 \(c\)。
注意到 \(c\gets c+m\) 时只要 \(mt\le k\) 则左侧每个 \(\lceil\frac{a_i+tc}{k}\rceil\) 增量 \(\le m\),那么必定更加合法,否则由于 \(1\sim m\) 至少操作一次,肯定无解。
那么枚举 \(c\bmod m\) 后就可以二分了。
注意到对每个 \(m\) 合法的 \(c\) 区间都不交且单调增加,所以只要找到首个合法的 \(m\) 就能快速计算答案了。
注意到每个 \(c\) 对应左式递增,因此从 \(c\) 可能的最小值开始,每次把 \(c\) 变成左式的值,直到 \(c\) 合法或超出上界。
对每个点迭代不超过 \(\log V\) 次,如果合法就二分答案,我们声称这样只有 \(\mathcal O(1)\) 个 \(m\) 能通过检验。
时间复杂度 \(\mathcal O(n\log n\log V)\)。
代码:
#include<bits/stdc++.h>
#define ll __int128
using namespace std;
namespace IO {
int ow,olim=(1<<21)-100;
char buf[1<<21],*p1=buf,*p2=buf,obuf[1<<21];
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
ll read() {
ll x=0,f=1; char c=gc();
while(!isdigit(c)) f=(c=='-'?-f:f),c=gc();
while(isdigit(c)) x=x*10+(c^48),c=gc();
return f*x;
}
void flush() {
fwrite(obuf,ow,1,stdout),ow=0;
}
void write(ll x) {
if(!x) obuf[ow++]='0';
else {
int t=ow;
for(;x;x/=10) obuf[ow++]=(x%10)^48;
reverse(obuf+t,obuf+ow);
}
if(ow>=olim) flush();
}
void putc(char c) {
obuf[ow++]=c;
if(ow>=olim) flush();
}
void putstr(const string &s) {
for(auto &c:s) obuf[ow++]=c;
if(ow>=olim) flush();
}
#undef gc
}
const int MAXN=1e6+5,B=150;
const ll inf=1e18,INF=1e36;
int n,id[MAXN];
ll k,t,a[MAXN],a0[MAXN],b[MAXN];
ll dv(ll x,ll y) {
return x>0?x/y:(x-y+1)/y;
}
struct BIT {
int tr[MAXN],s;
void add(int x) { for(;x<=n;x+=x&-x) ++tr[x]; }
int qry(int x) { for(s=0;x;x&=x-1) s+=tr[x]; return s; }
} T;
signed main() {
n=IO::read(),k=IO::read(),t=IO::read(),k+=t;
for(int i=1;i<=n;++i) a[i]=IO::read(),a0[i]=a[i];
sort(a+1,a+n+1,greater<>());
if(a[1]<=0) {
IO::putstr("0\n");
for(int i=1;i<=n;++i) IO::putc('0'),IO::putc(" \n"[i==n]);
return IO::flush(),0;
}
if(k==t) return puts("-1"),0;
if(t==0) {
ll z=0;
for(int i=1;i<=n;++i) if(a[i]>0) z+=(a[i]+k-1)/k;
IO::write(z),IO::putc('\n');
for(int i=1;i<=n;++i) IO::write(max((ll)0,(a0[i]+k-1)/k)),IO::putc(" \n"[i==n]);
return IO::flush(),0;
}
for(int i=1;i<=n;++i) b[i]=k-(a[i]%k+k-1)%k;
sort(b+1,b+n+1);
auto pos=[&](ll x) { return upper_bound(b+1,b+n+1,x)-b-1; };
ll z=inf,S=0;
for(int m=1;m<=n;++m) {
S+=dv(a[m]+k-1,k),T.add(pos(k-(a[m]%k+k-1)%k));
if(a[m+1]>0||k<=m*t) continue;
auto chk=[&](ll c) {
if(a[m]+c*t<=0) return false;
return c*t/k*m+S+T.qry(pos(c*t%k))<=c;
};
if(m<n) {
ll c0=max((ll)0,(-a[m])/t+1);
for(ll c1,_=0;a[m+1]+c0*t<=0&&_<=B;c0=c1,++_) {
c1=c0*t/k*m+S+T.qry(pos(c0*t%k));
if(c0>=c1) break;
}
if(a[m+1]+c0*t>0) continue;
}
for(int o=0;o<m;++o) if(z>o) {
ll w=(z-o-1)/m;
if(m<n) {
if(-a[m+1]-o*t<0) continue;
w=min(w,(-a[m+1]-o*t)/m/t);
}
if(!chk(w*m+o)) continue;
for(ll d=(ll)1<<63;d;d>>=1) if(w>=d&&chk((w-d)*m+o)) w-=d;
z=min(z,w*m+o);
}
if(z<inf) break;
}
if(z==inf) return puts("-1"),0;
IO::write(z),IO::putc('\n');
for(int i=1;i<=n;++i) {
IO::write(max((ll)0,(a0[i]+z*t+k-1)/k)),IO::putc(" \n"[i==n]);
}
IO::flush();
return 0;
}
M. [P12445] 数好图 (5)
首先处理 \(k=n\) 的问题,相当于每个点都有出度入度,简单容斥钦定一些点没有出度,然后记录前缀中有几个这样的点即可。
然后一般问题可以从一个 \(n=k\) 的图出发,插入若干能从 \(1\) 到达且不能到达 \(n\) 的点,以及若干无法从 \(1\) 到达的点。
容易分别 dp 前 \(i\) 个点中有 \(j\) 个这样的点的方案数,然后对每个 \(k\) 枚举两种点个数拼起来即可。
特判 \(k=0\)。
时间复杂度 \(\mathcal O(n^2)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2005;
int n,MOD,f[MAXN][MAXN],g[MAXN],h[MAXN][MAXN],pw[MAXN],w[MAXN][MAXN],z[MAXN];
int ksm(int a,int b) { int s=1; for(;b;a=1ll*a*a%MOD,b>>=1) if(b&1) s=1ll*s*a%MOD; return s; }
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>MOD;
for(int i=pw[0]=1;i<=n;++i) pw[i]=pw[i-1]*2%MOD;
f[1][0]=g[0]=1;
for(int i=1;i<=n;++i) for(int j=0;j<i;++j) if(f[i][j]) {
f[i+1][j+1]=(f[i+1][j+1]+1ll*f[i][j]*(pw[j+1]-1))%MOD;
f[i+1][j]=(f[i+1][j]+1ll*(MOD-f[i][j])*(pw[j]-1))%MOD;
g[i]=(g[i]+f[i][j])%MOD;
}
h[1][0]=w[1][0]=1;
for(int i=1;i<n;++i) for(int j=0;j<=i;++j) {
h[i+1][j]=(h[i+1][j]+h[i][j])%MOD;
h[i+1][j+1]=(h[i+1][j+1]+1ll*h[i][j]*(pw[i]-1))%MOD;
w[i+1][j]=(w[i+1][j]+1ll*w[i][j]*pw[j])%MOD;
w[i+1][j+1]=(w[i+1][j+1]+1ll*w[i][j]*pw[j])%MOD;
}
z[0]=ksm(2,n*(n-1)/2);
for(int i=2;i<=n;++i) {
for(int j=i;j<=n;++j) z[i]=(z[i]+1ll*h[j-1][j-i]*w[n-1][n-j]%MOD*pw[n-j])%MOD;
z[i]=1ll*z[i]*g[i]%MOD,z[0]=(z[0]+MOD-z[i])%MOD;
}
for(int i=0;i<=n;++i) cout<<z[i]<<" \n"[i==n];
return 0;
}
*N. [P13533] 寻找假币 (7.5)
可以把我们的操作看成询问区间中假币标号和。
首先我们有一个朴素的做法,对值域倍增分块,那么如果一个区间中假币个数 \(\le 1\) 可以直接判断出来,否则按 \([l,mid],[mid+1,r]\) 分治即可。
但是这样如果有两个标号接近的假币依然可以把询问次数变成 \(k\log n\) 级别。
考虑每次取恰当的 \(mid\) 使得两侧都有假币。
如果我们知道区间中的假币数量,则取 \(mid\) 为标号平均值必定合法。
那么对 \(c\) 二分,检验只要看对应的 \(mid\) 是否合法,可以证明询问次数为 \(2k\log k\) 级别。
如果每次根据区间内元素和动态计算 \(c\) 的范围,此时的询问次数非常接近题目限制,还需要一点常数优化。
我们用上递归过程中所有的信息来优化 \(c\) 的范围,直接在搜索的过程中记录 \(c\) 的上下界,并且分治的时候优先递归 \(c\) 取值范围较小的一侧。
还要去掉顶层值域分块,通过一些平凡的判断来处理 \(c=1\) 的影响。
时间复杂度 \(\mathcal O(k\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
vector <int> q;
ll qry(int x,ll z) {
cout<<"? "<<x<<endl;
ll o; cin>>o;
return 1ll*x*(x+1)/2-o-z;
}
int n,k;
void add(int x) { q.push_back(x); }
int ql(ll w,int L) {
int x=0;
for(;w>=L;++x,w-=L,++L);
return x;
}
int qr(ll w,int R) {
int x=0;
for(;R&&w>=R;++x,w-=R,--R);
return x;
}
int dfs(int l,int r,int cl,int cr,ll o,ll w) {
if(!w) return 0;
if(l<=w&&w<=2*l) return add(w),1;
cl=max({1,cl,qr(w,r)}),cr=min(cr,ql(w,l));
if(cr<=1) return add(w),1;
int cm=(cl+cr+1)>>1,mid=w/cm;
ll v=mid<l?0:mid>=r?w:qry(mid,o);
if(!v) return dfs(mid+1,r,cl,cm-1,o,w);
if(v==w) return dfs(l,mid,cm+1,cr,o,w);
int xl=max(qr(v,mid),cl-ql(w-v,mid+1)),xr=min(ql(v,l),cr-qr(w-v,r));
int yl=max(qr(w-v,r),cl-ql(v,l)),yr=min(ql(w-v,mid+1),cr-qr(v,mid)),c=0;
if(xr-xl<=yr-yl) {
c+=dfs(l,mid,xl,xr,o,v);
c+=dfs(mid+1,r,cl-c,cr-c,o+v,w-v);
} else {
c+=dfs(mid+1,r,yl,yr,o+v,w-v);
c+=dfs(l,mid,cl-c,cr-c,o,v);
}
return c;
}
void solve() {
cin>>n>>k,q.clear();
dfs(1,n,k,k,0,qry(n,0));
sort(q.begin(),q.end());
cout<<"! "; for(int x:q) cout<<x<<" ";
cout<<endl; int o; cin>>o;
}
signed main() {
int _; cin>>_;
while(_--) solve();
return 0;
}
O. [P11629] Nim 游戏 (5)
进行一些贪心,首先计算答案,如果 \(a_1=0\),可以证明我们的策略就是对异或和中每个为 \(1\) 的二进制位 \(k\) 调整,选择一个不含 \(2^k\) 且低位最大的数,把低位调整至 \(2^k\)。
可以用一些贪心说明该过程的正确性,进一步可以猜测所有的最优解都满足该形式,且我们会选 \(0\sim k-1\) 位最大的一个前缀,直接爆搜,如果选当前元素无法得到最优解就返回。
对每个 \(k\) 把所有数按低位取值提前排序,时间复杂度 \(\mathcal O(n\log V\log n+m\log^2 V)\)。
如果不存在 \(a_1=0\),那么当我们遇到某个所有 \(a_i\) 都为 \(1\) 的二进制位就无法继续该过程,所以要想办法手动生成 \(a_i=0\) 的元素。
可以选择一个高于异或和最高位的位置 \(k\),并且把两个数的该位都调整成 \(1\),还是类似地按低位从大到小枚举这样的两个数即可。
注意要对每个 \(k\) 都算出最优解并取最小值。
时间复杂度 \(\mathcal O(n\log V\log n+m\log^3V)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+5;
int n,m;
ll a[MAXN],w;
vector <map<int,ll>> f;
vector <array<ll,2>> b[64];
bool dfs(int k,ll S,const map<int,ll>&g,ll z) {
if(z>w||(int)f.size()>=m) return 0;
if(k<0) {
if(z<w) w=z,f.clear();
return f.push_back(g),1;
}
if(!(S>>k&1)) return dfs(k-1,S,g,z);
map<int,ll>h; bool ok=0; ll U=1ll<<k;
for(auto i:b[k]) if(!g.count(i[1])) {
h=g,h[i[1]]+=U-i[0];
if(dfs(k-1,S^U^i[0],h,z+U-i[0])) ok=1;
else return ok;
}
for(auto i:g) {
h=g,h[i.first]+=U;
if(dfs(k-1,S^U,h,z+U)) ok=1;
else return ok;
}
return ok;
}
void solve() {
cin>>n>>m,f.clear(),w=9e18;
ll S=0;
for(int i=1;i<=n;++i) cin>>a[i],S^=a[i];
for(int k=0;k<61;++k) {
ll U=(1ll<<k)-1; b[k].clear();
for(int i=1;i<=n;++i) if(!(a[i]>>k&1)) b[k].push_back({a[i]&U,i});
sort(b[k].begin(),b[k].end(),greater<>());
}
dfs(59,S,{},0);
for(int k=0;k<61;++k) if(!(S>>k)) {
ll U=1ll<<k; ++m;
for(auto i:b[k]) {
bool ok=0;
for(auto j:b[k]) if(i>j) {
if(!dfs(k-1,S^i[0]^j[0],{{i[1],U-i[0]},{j[1],U-j[0]}},U-i[0]+U-j[0])) break;
else ok=1;
}
if(!ok) break;
}
if((int)f.size()>(--m)) f.pop_back();
}
cout<<w<<"\n"<<f.size()<<"\n";
for(auto&o:f) {
cout<<o.size()<<"\n";
for(auto i:o) cout<<i.first<<" "; cout<<"\n";
for(auto i:o) cout<<i.second<<" "; cout<<"\n";
}
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int ty,_;
cin>>ty>>_;
while(_--) solve();
return 0;
}
*P. [P13346] 激光突击 (7.5)
首先我们的信息肯定只能传递第一次删掉哪个点。
首先菊花只要保留第一次没删掉的点就能得到菊花的根。
链则是每次删掉之前被选但没删掉的点。
考虑如何同时处理这两种矛盾的决策:我们取出链的终点,然后删掉交替删掉左侧和右侧最底下的边,那么我们处理每条边的时候考虑两个端点此前是否出现过,如果在前一条边出现就保留,否则在更前的边出现就删除。
那么原问题也采取类似的构造,取出直径中点定根,然后每层从下到上删点,由于每层点数 \(>1\),因此总能让跨层的时候不会连续删掉有父子关系的两个点。
此时如果一个点儿子中有非叶子节点就能通过以前的信息判定,所以要最开始快速处理所有儿子全是叶子的点。
考虑这些边怎么传递,我们无法直接传递信息,由于这些边无交,所以我们可以通过任意重排这些边的顺序传递一定的信息。
首先能算出每条边要删的点是较小还是较大值,然后按相邻两条边的字典序大小关系传递这两条边的删点状态是否相同。
先把所有边按字典序排列,然后把删点状态相同的每个连续段内部翻转,则当前边大于前一条边的字典序则删点状态改变。
注意我们删点的时候必须把所有兄弟也删掉。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1005;
int n;
namespace luotianyi {
vector <int> G[MAXN];
int fa[MAXN],d[MAXN],h[MAXN],lst=0;
void dfs(int u,int fz) {
fa[u]=fz,d[u]=d[fz]+1;
for(int v:G[u]) if(v^fz) dfs(v,u);
}
bool vis[MAXN];
int w(int x) { return (n+1)*min(x,fa[x])+max(x,fa[x]); }
vector <int> D[MAXN],S[MAXN];
void del(int u) {
vis[u]=true,cout<<u-1<<" ",lst=fa[u];
for(int x:S[fa[u]]) if(!vis[x]) del(x);
}
void solve() {
for(int i=1,u,v;i<n;++i) cin>>u>>v,++u,++v,G[u].push_back(v),G[v].push_back(u);
int o;
dfs(1,0),o=max_element(d+1,d+n+1)-d;
dfs(o,0),o=max_element(d+1,d+n+1)-d;
int k=d[o];
for(int i=0,u=o;i<k;++i,u=fa[u]) h[i]=u;
if(k&1) dfs(h[k/2],0);
else {
int l=h[k/2],r=h[k/2-1];
G[l].erase(find(G[l].begin(),G[l].end(),r));
G[r].erase(find(G[r].begin(),G[r].end(),l));
dfs(l,0),dfs(r,0);
}
for(int i=1;i<=n;++i) if(fa[i]) {
D[d[i]].push_back(i),S[fa[i]].push_back(i);
if(G[i].size()>1) vis[fa[i]]=true;
}
vector <int> f;
for(int i=1;i<=n;++i) if(fa[i]&&!vis[fa[i]]) f.push_back(i),vis[fa[i]]=true;
memset(vis,0,sizeof(vis));
sort(f.begin(),f.end(),[&](int i,int j){ return w(i)<w(j); });
for(auto l=f.begin(),r=l;l!=f.end();l=r) {
for(r=l;r!=f.end()&&(*r<fa[*r])==(*l<fa[*l]);++r);
reverse(l,r);
}
cout<<(f[0]>fa[f[0]])<<endl;
for(int x:f) del(x);
for(int i=n;i>1;--i) {
vector <int> q;
for(int u:D[i]) if(!vis[u]) q.push_back(u);
if(q.empty()) continue;
sort(q.begin(),q.end(),[&](int x,int y){ return S[x].size()>S[y].size(); });
if(q[0]==lst) swap(q[0],q[1]);
for(int x:q) if(!vis[x]) del(x);
}
if(k%2==0) cout<<(h[k/2]==lst?h[k/2-1]:h[k/2])-1<<" ";
cout<<endl;
}
}
int f[MAXN];
signed main() {
int _; cin>>_>>n;
if(_==1) return luotianyi::solve(),0;
string o; cin>>o;
int x=o[0]-'0';
array<int,2>lst={0,0},e;
for(int i=1;i<n;++i) {
cin>>e[0]>>e[1];
if(i==1) {
lst=e;
if(x) swap(e[0],e[1]);
} else if(f[e[0]]==i-1||f[e[1]]==i-1) {
if(f[e[0]]==i-1) swap(e[0],e[1]);
} else if(f[e[0]]||f[e[1]]) {
if(f[e[1]]) swap(e[0],e[1]);
} else {
x^=lst<e,lst=e;
if(x) swap(e[0],e[1]);
}
cout<<e[0]<<endl;
f[e[0]]=f[e[1]]=i;
}
return 0;
}
*Q. [P13531] 字符串问题 (7)
考虑分讨拆贡献,用 \([1,r]\) 的答案减去 \([1,l-1]\) 的答案,还要算出 \(a<l\le b\le r\) 的 \(t[a,b]\) 数量。
我们找到这样的 \((a,b)\) 中最大的 \(b\),则 \(t(b,r]\) 部分没有要减掉的贡献,用 \([1,b]\) 的答案减去 \([1,l-1]\) 的答案,再减去 \([l,b]\) 的答案就能得到 \(a<l\le b\le r\) 的 \(t[a,b]\) 数量。
注意到 \(t[a,b]\) 是一个模式串,而 \(t[l,b]\) 是模式串的后缀,这样的串只有 \(\mathcal O(|S|)\) 个,可以 AC 自动机处理。
时间复杂度 \(\mathcal O(|S|\Sigma+m\log |T|)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1005;
int n;
namespace luotianyi {
vector <int> G[MAXN];
int fa[MAXN],d[MAXN],h[MAXN],lst=0;
void dfs(int u,int fz) {
fa[u]=fz,d[u]=d[fz]+1;
for(int v:G[u]) if(v^fz) dfs(v,u);
}
bool vis[MAXN];
int w(int x) { return (n+1)*min(x,fa[x])+max(x,fa[x]); }
vector <int> D[MAXN],S[MAXN];
void del(int u) {
vis[u]=true,cout<<u-1<<" ",lst=fa[u];
for(int x:S[fa[u]]) if(!vis[x]) del(x);
}
void solve() {
for(int i=1,u,v;i<n;++i) cin>>u>>v,++u,++v,G[u].push_back(v),G[v].push_back(u);
int o;
dfs(1,0),o=max_element(d+1,d+n+1)-d;
dfs(o,0),o=max_element(d+1,d+n+1)-d;
int k=d[o];
for(int i=0,u=o;i<k;++i,u=fa[u]) h[i]=u;
if(k&1) dfs(h[k/2],0);
else {
int l=h[k/2],r=h[k/2-1];
G[l].erase(find(G[l].begin(),G[l].end(),r));
G[r].erase(find(G[r].begin(),G[r].end(),l));
dfs(l,0),dfs(r,0);
}
for(int i=1;i<=n;++i) if(fa[i]) {
D[d[i]].push_back(i),S[fa[i]].push_back(i);
if(G[i].size()>1) vis[fa[i]]=true;
}
vector <int> f;
for(int i=1;i<=n;++i) if(fa[i]&&!vis[fa[i]]) f.push_back(i),vis[fa[i]]=true;
memset(vis,0,sizeof(vis));
sort(f.begin(),f.end(),[&](int i,int j){ return w(i)<w(j); });
for(auto l=f.begin(),r=l;l!=f.end();l=r) {
for(r=l;r!=f.end()&&(*r<fa[*r])==(*l<fa[*l]);++r);
reverse(l,r);
}
cout<<(f[0]>fa[f[0]])<<endl;
for(int x:f) del(x);
for(int i=n;i>1;--i) {
vector <int> q;
for(int u:D[i]) if(!vis[u]) q.push_back(u);
if(q.empty()) continue;
sort(q.begin(),q.end(),[&](int x,int y){ return S[x].size()>S[y].size(); });
if(q[0]==lst) swap(q[0],q[1]);
for(int x:q) if(!vis[x]) del(x);
}
if(k%2==0) cout<<(h[k/2]==lst?h[k/2-1]:h[k/2])-1<<" ";
cout<<endl;
}
}
int f[MAXN];
signed main() {
int _; cin>>_>>n;
if(_==1) return luotianyi::solve(),0;
string o; cin>>o;
int x=o[0]-'0';
array<int,2>lst={0,0},e;
for(int i=1;i<n;++i) {
cin>>e[0]>>e[1];
if(i==1) {
lst=e;
if(x) swap(e[0],e[1]);
} else if(f[e[0]]==i-1||f[e[1]]==i-1) {
if(f[e[0]]==i-1) swap(e[0],e[1]);
} else if(f[e[0]]||f[e[1]]) {
if(f[e[1]]) swap(e[0],e[1]);
} else {
x^=lst<e,lst=e;
if(x) swap(e[0],e[1]);
}
cout<<e[0]<<endl;
f[e[0]]=f[e[1]]=i;
}
return 0;
}
R. [P11436] 生成树 (4.5)
首先答案不超过每个点入度乘积,而和为 \(100\) 的数乘积不超过 \(4\times 3^{32}=7.4\times 10^{15}\),所以每个点的入度必定取到该数组。
如果每个点的入边都从 \(1\) 连,那么能取到 \(\prod \deg(u)\),考虑如何调整。
观察到如果产生一个环,那么会减掉环外点的 \(\prod \deg(u)\),考虑怎么样让所有环不能共存,很显然只要让每个环的点集都是后缀即可。
具体来说,贪心地确定 \(d_2\sim d_n\) 使得 \(\prod d\ge k\) 且 \(\sum d\) 尽可能小,连 \(2\to3\to\cdots\to n\) 的链,然后给答案减掉 \(\prod_{j\le i} d_j\) 只要把 \(n\) 向 \(i+1\) 连边即可,剩余的 \(d_i\) 从 \(1\) 连边。
那么按 \(i\) 从高到低调整即可,显然这是一个类似三进制的表示方法,肯定合法。
时间复杂度 \(\mathcal O(n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll f[105],d[105],m;
void solve() {
cin>>m,f[1]=1,d[2]=1;
int n=2;
while(f[n-1]*d[n]<m) {
if(d[n]<3||n==34) ++d[n];
else f[n]=f[n-1]*d[n],d[++n]=1;
}
f[n]=f[n-1]*d[n];
vector <array<int,2>> E;
for(int i=2;i<n;++i) --d[i+1],E.push_back({i,i+1});
ll x=f[n];
for(int i=n-1;i>=1;--i) while(x-f[i]>=m) x-=f[i],E.push_back({n,i+1}),--d[i+1];
for(int i=2;i<=n;++i) while(d[i]--) E.push_back({1,i});
cout<<n<<" "<<E.size()<<"\n";
for(auto e:E) cout<<e[0]<<" "<<e[1]<<"\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
S. [P13508] Burenka and Pether (4)
首先每个点 \(i\) 能到达的点应该是 \([i,r_i]\) 范围内所有 \(a_j>a_i\) 的点 \(j\)。
考虑如何进一步刻画每个点的决策,首先 \(i\) 必须跳到 \(a_j\in(a_i,a_v]\) 的点,并且我们注意到如果有两个这样的点 \(j,k\in[i,r_i]\) 且 \(a_j>a_k\),那么 \(r_j\ge r_k\)。
所以可以证明策略就是跳到所有合法点中 \(a\) 的最大值。
对 \(a\) 值域分块,如果 \(a_v\in [l,r]\),则按 \(a_v\) 降序处理,动态维护出每个点跳进 \([l,a_v]\) 的时刻,然后对 \(a_i\ge l\) 的 \(\sqrt n\) 个点暴力维护。
具体来说先维护每个点跳 \(<l\) 的边时首次跳进 \([l,r]\) 的位置,然后模拟的时候对于没法跳到 \([l,a_v]\) 的点,\(a_v\) 减小后肯定也跳不进 \([l,a_v]\),所以用并查集把该点和其后继缩起来。
时间复杂度 \(\mathcal O((n+q)\sqrt n)\)。
更优的做法是在值域上考虑跳跃的过程,然后用 CDQ 分治维护跨过 \(mid\) 的跳跃,可以做到 \(\mathcal O((n+q)\log^2n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e5+5;
int n,D,_,q,B,a[MAXN],b[MAXN],c[MAXN],z[MAXN];
vector <array<int,2>> qy[MAXN];
void init() {
static int L[MAXN],R[MAXN];
for(int i=1;i<=n;++i) L[i]=i-1,R[i]=i+1;
set <int> S; S.insert(n+1);
for(int i=n;i>=1;--i) {
int x=b[i];
c[x]=min(n,*S.lower_bound(x)+D),R[L[x]]=R[x],L[R[x]]=L[x];
if(R[x]-L[x]>D) S.insert(L[x]);
}
}
int id[MAXN],st[MAXN],f[MAXN],mx[MAXN],fa[MAXN],d[MAXN],e[MAXN];
void find(int x) {
if(fa[x]==x) return ;
find(fa[x]),d[x]+=d[fa[x]],fa[x]=fa[fa[x]];
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>D>>_,B=min(n,2*(int)sqrt(n));
for(int i=1;i<=n;++i) cin>>a[i],b[a[i]]=i;
cin>>q;
for(int i=1,u,v;i<=q;++i) {
cin>>z[i]>>u>>v;
if(a[u]<a[v]) qy[a[v]].push_back({u,i});
else z[i]=0;
}
init();
for(int i=1;i<=n;++i) f[i]=a[i];
for(int L=1,R;L<=n;L=R+1) {
R=min(n,L+B-1);
int k=0;
for(int i=1;i<=n;++i) {
if(L<=a[i]&&a[i]<=R) st[++k]=a[i];
id[i]=k;
}
for(int i=R;i;--i) {
int x=b[i];
if(id[c[x]]>id[x-1]) fa[x]=x,d[x]=0;
else fa[x]=fa[b[f[x]]],d[x]=d[b[f[x]]]+1;
}
for(int v=R;v>=L;--v) {
e[k+1]=k+1;
for(int i=k,p=k+1;i>=1;--i) e[i]=p=(st[i]<=v?i:p);
for(auto o:qy[v]) {
int u=o[0],s=0;
while(a[u]<v) {
find(u),s+=d[u],u=fa[u];
int x=f[u];
for(int i=e[id[u-1]+1];i<=id[c[u]];i=e[i+1]) if(st[i]<=v) x=max(x,st[i]);
if(x==a[u]) { z[o[1]]=0; break; }
if(x<L) fa[u]=b[x],d[u]=1;
++s,u=b[x];
}
if(z[o[1]]) z[o[1]]=z[o[1]]==1?1:s;
}
}
for(int i=n;i>=1;--i) {
if(L<=a[i]&&a[i]<=R) {
for(int j=id[i];j<=k;++j) mx[j]=max(mx[j],a[i]);
}
if(id[c[i]]>id[i-1]) f[i]=max(f[i],mx[id[c[i]]]);
}
}
for(int i=1;i<=q;++i) cout<<z[i]<<"\n";
return 0;
}
*T. [P11613] 覆盖 (7.5)
首先转成最大独立集,对图 \(\bmod p\) 计数有经典做法,取出前两个点,如果这两个点邻域不同则交换之,得到一个相等的图。
因此这两个点邻域相同,讨论两点间是否有边,如果有边那么两个点至多选一个,看成删除一个点,否则必定同时选或不选,生成一个大小为 \(2\) 的点。
那么不断合并大小相同的两个点,最终图上会剩若干大小不同的点,设为 \(2^{a_1}\sim 2^{a_k}\)。
很显然我们的策略就是按 \(a\) 从大到小贪心,每个点如果邻域没选就选上。
那么记 \(m=\sum 2^{a_i}\),先考虑 \(n\) 个点得到 \(m\) 个这样的组的方案数 \(f(n,m)\)。
记 \(\mathrm{lowbit}(m)=2^k\),考虑加入最后一个点时 \(m\) 的变化过程,首先必定存在 \(2^0\sim 2^{k-1}\) 并且不断选择合成才能不生成 \(<2^k\) 的点,而生成出 \(2^k\) 后,如果原来不存在 \(2^k\),那么合成过程停止;否则如果把两个 \(2^k\) 合成就不存在大小为 \(2^k\) 的点了,所以原本有 \(2^k\) 的情况下会删除一个 \(2^k\) 的点。
那么 \(f(n,m)=f(n-1,m-1),f(n-1,m+2^k-1)\)。
然后考虑 \(m\) 向某个 \(w\subseteq m\) 转移的方案数,考虑方案数,如果有一个 \(2^{a_i}\not\in w\),则 \(a_j<a_i\) 的点可以任意选择是否与 \(i\) 连边。
所以 \(w\not\in\{m,m-2^k\}\) 时必定有偶数种连边方案,所以 \(f(n,m)\) 只能向 \((n,m),(n,m-2^k)\) 转移答案。
时间复杂度 \(\mathcal O(n^2)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1<<14|5;
bitset <MAXN> f,g,h,z;
vector <array<int,2>> qy[MAXN];
int n,q;
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>q;
for(int i=1,x,y;i<=q;++i) cin>>x>>y,n=max(n,x),qy[x].push_back({x-y,i});
f[0]=1;
for(int i=1;i<=n;++i) {
h.reset(),g.reset();
for(int x=1;x<=i;++x) {
h[x]=f[x-1]^f[x+(x&-x)-1];
if(h[x]) g.flip(x),g.flip(x&(x-1));
}
f=h;
for(auto o:qy[i]) z[o[1]]=g[o[0]];
}
for(int i=1;i<=q;++i) cout<<z[i]<<"\n";
return 0;
}
U. [P13526] 庆典 (3.5)
首先 \(f_{u,d}\) 表示 \(u\) 子树最大深度为 \(d\) 的答案,首先转移 \(f'_{v,d}\gets \max(f_{v,d},f_{v,d-1}+w)\),然后 $f_{u,\max(x,y)}\gets [x+y\le x] (f_{u,x}+f_{v,y}) $。
可以猜测答案具有凸性,则一转移容易用 std::multiset 维护,而二转移只要做到 \(\min(|f_u|,|f_v|)\) 级别的复杂度即可。
设 \(|f_u|=s,|f_v|=t\),则 \(s \le 2t+2\) 时可以暴力 dp,否则注意到只有前后 \(t\) 个元素转移特殊,中间的元素都是全局加 \(f_{v,t}\),把这 \(\mathcal O(t)\) 个元素取出来 dp 即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5;
multiset <ll,greater<>> f[MAXN];
ll g[MAXN],w[MAXN],z[MAXN];
int n,k,fa[MAXN];
vector <int> G[MAXN];
ll a[MAXN],b[MAXN],c[MAXN];
void merge(int u,int v) {
if(f[v].size()>f[u].size()) swap(g[u],g[v]),swap(f[u],f[v]);
int s=f[u].size(),t=f[v].size();
if(2*t+2>=s) {
memset(c,0,(s+1)<<3),s=t=0,a[0]=b[0]=0;
for(ll x:f[u]) ++s,a[s]=a[s-1]+x;
for(ll x:f[v]) ++t,b[t]=b[t-1]+x;
for(int i=0;i<=s;++i) c[i]=max(c[i],a[i]+b[min({t,i,k-i})]);
for(int i=0;i<=t;++i) c[i]=max(c[i],b[i]+a[min({s,i,k-i})]);
for(int i=1;i<=s;++i) c[i]=max(c[i],c[i-1]);
g[u]=c[s];
for(int i=s;i;--i) c[i]-=c[i-1];
f[u]=multiset<ll,greater<>>(c+1,c+s+1);
return ;
}
memset(c,0,(t+2)<<3),t=0,a[0]=b[0]=0;
for(ll x:f[v]) ++t,b[t]=b[t-1]+x;
auto it=f[u].begin();
for(int i=1;i<=t+1;++i,++it) a[i]=a[i-1]+*it;
f[u].erase(f[u].begin(),it),c[t+1]=a[t+1]+b[t];
for(int i=0;i<=t;++i) c[i]=max(a[i]+b[min({t,i,k-i})],b[i]+a[min({t,i,k-i})]);
for(int i=0;i<=t;++i) c[i+1]=max(c[i+1],c[i]),f[u].insert(c[i+1]-c[i]);
it=f[u].end(),a[0]=g[u];
for(int i=1;i<=t+1;++i) a[i]=a[i-1]-*--it;
f[u].erase(it,f[u].end()),memset(c,0,(t+2)<<3);
for(int i=0;i<=t+1;++i) c[i]=a[i]+b[min({t,s-i,k-(s-i)})];
for(int i=t;~i;--i) c[i]=max(c[i],c[i+1]);
for(int i=0;i<=t;++i) f[u].insert(c[i]-c[i+1]);
g[u]=c[0];
}
void dfs(int u) {
for(int v:G[u]) {
dfs(v),f[v].insert(w[v]),g[v]+=w[v];
if((int)f[v].size()>k) g[v]-=*f[v].rbegin(),f[v].erase(--f[v].end());
merge(u,v);
}
z[u]=g[u];
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=2;i<=n;++i) cin>>fa[i]>>w[i],G[fa[i]].push_back(i);
dfs(1);
for(int i=1;i<=n;++i) cout<<z[i]<<"\n";
return 0;
}
V. [P13919] 黑暗密室 (4)
首先走到根上,然后考虑如何两次操作使深度 \(+2\)。
如果四个孙子都存在,就选取最左边的一个问一次即可,否则简单分讨一下存在情况也能简单构造策略。
此时询问次数 \(\dfrac n2+1\)。
考虑优化掉在根上的询问,考虑用前两次询问求出答案的深度,以及在第四层的哪个子树中。
首先暴力 dfs 求出前四层的图及其全源最短路,那么询问 \((x,y)\) 合法当前仅当对于第四层的每个 \(u\) 其 \(\mathrm{dis}(x,u)-\mathrm{dis}(y,u)\) 都不同。
暴力枚举 \(x,y\),可以证明这样的 \(x,y\) 总是存在。
时间复杂度 \(\mathcal O(n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int B=1000;
const string OP[5]={"up","right","downright","downleft","left"};
string go(int x) { cout<<OP[x]<<endl; string o; cin>>o; return o; }
int qry() { cout<<"app"<<endl; int o; cin>>o; return o; }
string s,l,r;
int d,ls[32],rs[32],f[32][32],inf=1e9;
bool g[32];
void add(int u,int v,int w) { f[u][v]=f[v][u]=min(f[u][v],w); }
void dfs(int u) {
g[u]=1;
if(u>=16) return ;
if(s[3]=='1') s=go(3),dfs(u<<1),s=go(0);
if(s[2]=='1') s=go(2),dfs(u<<1|1),s=go(0);
}
void solve() {
for(int x;d;) {
if(s[2]=='0') { s=go(3),--d; continue; }
if(s[3]=='0') { s=go(2),--d; continue; }
if(d==1) {
s=go(3),x=qry();
if(x!=d-1) s=go(1);
break;
}
r=go(2),l=go(4);
int c=l[2]-'0'+l[3]-'0'+r[2]-'0'+r[3]-'0';
if(c==4) {
s=go(3),x=qry();
for(int t=d-2;t<x;++t) s=go(1);
d-=2;
} else if(c==3) {
if(l[3]=='0') {
s=go(2),x=qry();
for(int t=d-2;t<x;++t) s=go(1);
} else if(r[2]=='0') {
s=go(3),x=qry();
for(int t=d-2;t<x;++t) s=go(1);
} else if(l[2]=='0') {
go(1),s=go(2),x=qry();
if(x==d-1) s=go(4);
else if(x==d+1) go(0),go(4),s=go(3);
} else {
s=go(3),x=qry();
if(x==d-1) s=go(1);
else if(x==d+1) go(0),go(1),s=go(2);
}
d-=2;
} else {
if(r[2]=='0'&&r[3]=='0') { s=l,--d; continue; }
if(l[2]=='0'&&l[3]=='0') { s=go(1),--d; continue; }
s=l,x=qry();
if(x!=d-1) s=go(1);
--d;
}
}
cout<<"here"<<endl;
}
signed main() {
for(cin>>s;s[0]=='1';s=go(0));
dfs(1);
int ct=count(g+16,g+32,1);
for(int i=1;i<32;++i) for(int j=1;j<32;++j) f[i][j]=i==j?0:inf;
for(int i=2;i<32;++i) if(g[i]) {
add(i>>1,i,B+1);
if((i&(i-1))&&g[i-1]) add(i-1,i,1);
}
for(int k=1;k<32;++k) for(int i=1;i<32;++i) for(int j=1;j<32;++j) f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
for(int x=1;x<32;++x) if(g[x]) for(int y=x+1;y<32;++y) if(g[y]) {
map <int,int> o;
for(int i=16;i<32;++i) if(g[i]) o[f[x][i]%B-f[y][i]%B]=i;
if((int)o.size()!=ct) continue;
for(int t=__lg(x)-1;~t;--t) s=go(3-(x>>t&1));
int X=qry();
for(int t=__lg(x)-1;~t;--t) s=go(0);
if(X<=990) return d=qry(),solve(),0;
for(int t=__lg(y)-1;~t;--t) s=go(3-(y>>t&1));
int Y=qry();
for(int t=__lg(y)-1;~t;--t) s=go(0);
d=X-f[x][o[X-Y]]%B;
for(int t=3;~t;--t) s=go(3-(o[X-Y]>>t&1));
return solve(),0;
}
d=qry(),solve();
return 0;
}
W. [P13548] Air Reform (2.5)
直接在 Kruskal 重构树上做 Kruskal 算法,那么按值从小到大,合并一个点左右子树时把子树间存在的边都连上即可。
动态维护左右子树的点集以及连通块集合,暴力枚举轻子树中的每个点 \(u\),以及重子树的每个连通块有几个和 \(u\) 无边的点。
注意到连边后每保留一个连通块至少对应原图中一条 \(u\) 的出边,所以每次暴力访问所有重子树连通块均摊复杂度正确。
时间复杂度 \(\mathcal O(m\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e5+5;
int n,m,dsu[MAXN*2],siz[MAXN],dso[MAXN],ct[MAXN];
bool inq[MAXN];
basic_string<int> G[MAXN],f[MAXN],g[MAXN];
int fond(int x) { return dso[x]^x?dso[x]=fond(dso[x]):x; }
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
int ch[MAXN*2][2],w[MAXN*2],dfn[MAXN*2],dcnt,st[MAXN*2][20];
int cmp(int x,int y) { return dfn[x]<dfn[y]?x:y; }
void dfs(int u,int fz) {
dfn[u]=++dcnt,st[dcnt][0]=fz;
if(u>n) for(int v:ch[u]) dfs(v,u);
}
int LCA(int x,int y) {
if(x==y) return x;
int l=min(dfn[x],dfn[y])+1,r=max(dfn[x],dfn[y]),k=__lg(r-l+1);
return cmp(st[l][k],st[r-(1<<k)+1][k]);
}
void solve() {
cin>>n>>m;
for(int i=1;i<=n;++i) dsu[i]=dso[i]=i,siz[i]=1,inq[i]=0,f[i]=g[i]={i},G[i].clear();
vector<array<int,4>> E,Z;
for(int i=1,u,v,z;i<=m;++i) cin>>u>>v>>z,G[u].push_back(v),G[v].push_back(u),E.push_back({z,u,v,i});
sort(E.begin(),E.end());
for(auto e:E) {
int L=fond(e[1]),R=fond(e[2]);
if(L==R) continue;
if(g[L].size()<g[R].size()) swap(L,R);
dso[R]=L;
for(int u:g[R]) {
int h=find(u); basic_string<int> Q,N;
for(int x:f[L]) if(x!=h) Q+=x,ct[x]=siz[x],inq[x]=1;
for(int x:G[u]) if(inq[find(x)]) --ct[find(x)];
for(int x:Q) {
if(!ct[x]) N+=x;
else Z.push_back({e[0],h,x}),siz[h]+=siz[x],dsu[x]=h;
ct[x]=0,inq[x]=0;
}
N+=h,f[L].swap(N);
}
g[L]+=g[R];
}
int h=n; iota(dsu+1,dsu+2*n,1);
for(auto e:Z) {
int u=find(e[1]),v=find(e[2]);
w[++h]=e[0],ch[h][0]=u,ch[h][1]=v,dsu[u]=dsu[v]=h;
}
dcnt=0,dfs(h,0);
for(int k=1;k<20;++k) for(int i=1;i+(1<<k)-1<=dcnt;++i) {
st[i][k]=cmp(st[i][k-1],st[i+(1<<(k-1))][k-1]);
}
sort(E.begin(),E.end(),[&](auto x,auto y){ return x[3]<y[3]; });
for(int i=0;i<m;++i) cout<<w[LCA(E[i][1],E[i][2])]<<" \n"[i==m-1];
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _,ty; cin>>_>>ty;
while(_--) solve();
return 0;
}
X. [P14021] 博德之跃 2 (4.5)
首先注意到如果选出的 \(S,\mathrm{rev}(S)\) 有交,则原串为回文串,取最长真前缀肯定合法且最优。
所以当某个时刻得到回文串后的操作次数可以直接计算,而得到回文串之前每次串长减半,只有 \(\mathcal O(\log n)\) 次操作。
考虑倒过来从最终的回文串 \(S\) 出发,每次操作会找到后面首个 \(\mathrm{rev}(S)\) 的位置并更新,容易用后缀数据结构优化该过程。
不难发现确定 \(S\) 后左端点不变,因此枚举左端点 \(l\),显然所有 \(l\) 开头的回文串中最长的一个最优,那么处理出其长度后暴力拓展即可。
时间复杂度 \(\mathcal O(n\log ^2n)\)。
更优的做法是我们只会关心 \(S+\mathrm{rev}(S)\) 上每个 \(\mathrm{endpos}\) 等价类中的最长串,然后简单维护即可做到 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
struct SA {
char s[MAXN];
int n,m,sa[MAXN],rk[MAXN],ct[MAXN],t[MAXN],f[MAXN][20];
void init() {
m=27,memset(ct,0,(m+1)<<2),s[n+1]=0;
for(int i=1;i<=n;++i) rk[i]=s[i]-'a'+1,++ct[rk[i]];
for(int i=1;i<=m;++i) ct[i]+=ct[i-1];
for(int i=n;i;--i) sa[ct[rk[i]]--]=i;
for(int k=1;;k<<=1) {
for(int i=1;i<=k;++i) t[i]=i+n-k;
for(int i=1,h=k;i<=n;++i) if(sa[i]>k) t[++h]=sa[i]-k;
memset(ct,0,(m+1)<<2);
for(int i=1;i<=n;++i) ++ct[rk[i]];
for(int i=1;i<=m;++i) ct[i]+=ct[i-1];
for(int i=n;i;--i) sa[ct[rk[t[i]]]--]=t[i];
memcpy(t,rk,(n+1)<<2),m=0,t[n+1]=0;
for(int i=1;i<=n;++i) {
rk[sa[i]]=m+=(i==1||t[sa[i]]!=t[sa[i-1]]||t[min(n+1,sa[i]+k)]!=t[min(n+1,sa[i-1]+k)]);
}
if(m==n) break;
}
for(int i=1,k=0;i<=n;++i) {
for(k=max(k-1,0);s[i+k]==s[sa[rk[i]-1]+k];++k);
f[rk[i]][0]=k;
}
for(int k=1;k<20;++k) for(int i=1;i+(1<<k)-1<=n;++i) {
f[i][k]=min(f[i][k-1],f[i+(1<<(k-1))][k-1]);
}
}
int lcs(int x,int y) {
if(x==y) return n-x+1;
int l=min(rk[x],rk[y])+1,r=max(rk[x],rk[y]),k=__lg(r-l+1);
return min(f[l][k],f[r-(1<<k)+1][k]);
}
} S;
struct Segt {
int sz[MAXN*20],ls[MAXN*20],rs[MAXN*20],tot;
void init() {
for(int i=1;i<=tot;++i) sz[i]=ls[i]=rs[i]=0;
tot=0;
}
void ins(int x,int l,int r,int q,int &p) {
sz[p=++tot]=sz[q]+1;
if(l==r) return ;
int mid=(l+r)>>1;
if(x<=mid) ins(x,l,mid,ls[q],ls[p]),rs[p]=rs[q];
else ins(x,mid+1,r,rs[q],rs[p]),ls[p]=ls[q];
}
int qry(int x,int l,int r,int q,int p) {
if(sz[p]==sz[q]) return S.n+1;
if(l==r) return l;
int mid=(l+r)>>1,t=S.n+1;
if(x<=mid) t=qry(x,l,mid,ls[q],ls[p]);
return t>S.n?qry(x,mid+1,r,rs[q],rs[p]):t;
}
} T;
int n,R[MAXN],h[MAXN],dsu[MAXN];
int find(int x) { return x^dsu[x]?dsu[x]=find(dsu[x]):x; }
void upd(int l,int r,int w,int z) {
for(int x=find(l);x<=r;dsu[x]=x+1,x=find(x)) R[x]=w-x,h[x]=z;
}
int pr[MAXN],nx[MAXN],rt[MAXN];
string s;
int qry(int l,int r) {
int x=S.rk[2*n+2-r],y=x;
for(int i=19,k=1<<i;~i;k>>=1,--i) {
if(x>k&&S.f[x-k+1][i]>=r-l+1) x-=k;
if(y+k<=S.n&&S.f[y+1][i]>=r-l+1) y+=k;
}
int o=T.qry(r+1,1,S.n,rt[y],rt[x-1]);
return o>n?-1:o+r-l;
}
void solve() {
cin>>s,n=s.size(),s=" "+s+"{"+s,T.init();
reverse(s.begin()+n+2,s.end()),S.n=2*n+1;
for(int i=1;i<=S.n;++i) S.s[i]=s[i];
S.init(),pr[1]=nx[n]=1;
for(int i=1;i<=S.n;++i) T.ins(S.sa[i],1,S.n,rt[i-1],rt[i]);
for(int i=2;i<=n;++i) pr[i]=s[i]==s[i-1]?pr[i-1]+1:1;
for(int i=n-1;i;--i) nx[i]=s[i]==s[i+1]?nx[i+1]+1:1;
for(int i=1;i<=n+1;++i) dsu[i]=i;
for(int i=n;i>=1;--i) {
int d=S.lcs(i,2*n+2-i);
upd(i-d+1,i,2*i,2*min(pr[i],nx[i])-1);
if(i>1&&s[i]==s[i-1]) d=S.lcs(i,2*n+3-i),upd(i-d,i-1,2*i-1,2*min(pr[i-1],nx[i]));
}
int z=0;
vector <array<int,2>> e;
for(int i=1;i<=n;++i) e.push_back({(R[i]-i+1-h[i])/2+h[i]-1,i});
sort(e.begin(),e.end(),greater<>());
for(auto o:e) {
int w=o[0],i=o[1];
if(w+__lg((n-R[i])/(R[i]-i+1)+1)<=z) continue;
for(int r=R[i];~r;r=qry(i,r),++w);
z=max(z,w-1);
}
cout<<z<<"\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
Y. [P12403] 象掌兽 (4)
首先树上 \(s,t\) 距离 \(>2\) 那么可以相向移动,所以只要暴力枚举距离 \(\le 2\) 的点对。
对于基环树上的问题,类似地如果 \(s,t\) 都不在环上,则只要考虑距离 \(\le 2\) 的情况。
否则我们只要考虑存在一个不完全同色的环的情况。
枚举该环,必定有一个点在环上,设为 \(s\),则分讨环上是否有黑色点。
如果有,则粉点和黑点都是区间,且两个区间中间夹了 \(\le 1\) 个蓝色点。
枚举粉色区间后只有 \(\mathcal O(n)\) 种要检验的区间对,我们要在长度较小的区间上任意一个点的子树中找到深度为 \(\frac 12\) 区间长度差的点。
那么换根 dp 求仙人掌上每个点子树内外最大深度,然后单调队列维护区间最深点即可判断。
如果没有黑点,则蓝点和黑点都是区间,且粉点一定是蓝点区间两个端点的某个子树,枚举该子树并算出黑点范围,然后要在该子树中找深度为某个值的点,依然可以预处理。
时间复杂度 \(\mathcal O(n+m)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=4e5+5;
vector <int> G[MAXN],E[MAXN];
int n,m,vc,A,B,dfn[MAXN],low[MAXN],dcnt,st[MAXN],tp;
bool ins[MAXN];
void tarjan(int u) {
dfn[u]=low[u]=++dcnt,ins[st[++tp]=u]=1;
for(int v:E[u]) {
if(!dfn[v]) {
tarjan(v),low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]) {
for(G[u].push_back(++vc);ins[v];ins[st[tp--]]=0) G[vc].push_back(st[tp]);
}
} else low[u]=min(low[u],dfn[v]);
}
}
int siz[MAXN],fa[MAXN],c[MAXN],f[MAXN];
void dfs1(int u,int fz) {
siz[u]=u<=n,fa[u]=fz;
for(int v:G[u]) dfs1(v,u),siz[u]+=siz[v];
if(u<=n) for(int v:G[u]) f[u]=max(f[u],f[v]);
else {
c[u]=G[u].size()+1;
for(int i=0;i<c[u]-1;++i) f[u]=max(f[u],f[G[u][i]]+min(i+1,c[u]-1-i));
}
}
int g[MAXN],q[MAXN],w[MAXN];
void dfs2(int u) {
if(u<=n) {
int fi=g[u],se=0;
for(int v:G[u]) se=max(se,min(fi,f[v])),fi=max(fi,f[v]);
for(int v:G[u]) g[v]=(f[v]==fi?se:fi);
} else {
w[c[u]-1]=g[u];
for(int i=0;i<c[u]-1;++i) w[i]=w[i+c[u]]=f[G[u][i]];
int hd=1,tl=0,d=c[u]/2;
for(int o=2;o--;) {
for(int i=0;i<2*c[u]-1;++i) {
while(hd<=tl&&q[hd]<i-d) ++hd;
if(i>=c[u]) g[G[u][i-c[u]]]=max(g[G[u][i-c[u]]],i-q[hd]+w[q[hd]]);
while(hd<=tl&&w[q[tl]]-q[tl]<=w[i]-i) --tl;
q[++tl]=i;
}
reverse(w,w+2*c[u]-1),reverse(G[u].begin(),G[u].end());
}
}
for(int v:G[u]) dfs2(v);
}
vector <int> H[MAXN];
int d[MAXN],b[MAXN],pa[MAXN],pb[MAXN],xa[MAXN],xb[MAXN];
ll s[MAXN];
bool vis[MAXN];
int in(int x,int e) {
queue <array<int,2>> Q; Q.push({x,0});
while(1) {
auto u=Q.front(); Q.pop();
if(u[1]==e) return u[0];
for(int v:E[u[0]]) if(!vis[v]) vis[v]=true,Q.push({v,u[1]+1});
}
}
void solve() {
cin>>n>>m>>A>>B,vc=n;
for(int i=1,u,v;i<=m;++i) cin>>u>>v,E[u].push_back(v),E[v].push_back(u);
tarjan(1),dfs1(1,0);
for(int u=1;u<=n;++u) {
if(!fa[u]||G[fa[u]].size()>1) continue;
if(siz[u]==A&&n-siz[u]==B) return cout<<u<<" "<<fa[fa[u]]<<"\n",void();
if(siz[u]==B&&n-siz[u]==A) return cout<<fa[fa[u]]<<" "<<u<<"\n",void();
}
for(int u=1;u<=n;++u) {
if(fa[u]&&G[fa[u]].size()==1) H[n-siz[u]].push_back(fa[fa[u]]);
for(int v:G[u]) if(G[v].size()==1) H[siz[v]].push_back(G[v][0]);
if(A==B&&H[A].size()>1) return cout<<H[A][0]<<" "<<H[A][1]<<"\n",void();
if(A!=B&&H[A].size()&&H[B].size()) return cout<<H[A][0]<<" "<<H[B][0]<<"\n",void();
H[n-siz[u]].clear();
for(int v:G[u]) H[siz[v]].clear();
}
dfs2(1);
for(int u=n+1;u<=vc;++u) if(G[u].size()>1) {
for(int i=0;i<c[u]-1;++i) s[i+1]=s[i+c[u]+1]=siz[G[u][i]],d[i+1]=d[i+c[u]+1]=f[G[u][i]],b[i+1]=b[i+c[u]+1]=G[u][i];
s[c[u]]=s[2*c[u]]=n-siz[u],d[c[u]]=d[2*c[u]]=g[u],b[0]=b[c[u]]=b[2*c[u]]=fa[u];
for(int i=1;i<=2*c[u];++i) s[i]+=s[i-1],pa[i]=pb[i]=xa[i]=xb[i]=0;
for(int i=1,j=1,k=1;i<=c[u];++i) {
while(s[j]-s[i-1]<A) ++j;
while(s[k]-s[i-1]<B) ++k;
if(s[j]-s[i-1]==A) pa[i]=j;
if(s[k]-s[i-1]==B) pb[i]=k;
}
for(int i=2*c[u],j=i,k=i;i>c[u];--i) {
while(s[i]-s[j-1]<A) --j;
while(s[i]-s[k-1]<B) --k;
if(s[i]-s[j-1]==A) pa[i]=j;
if(s[i]-s[k-1]==B) pb[i]=k;
}
for(int i=2*c[u],hd=1,tl=0;i;--i) {
while(hd<=tl&&d[q[tl]]<=d[i]) --tl;
q[++tl]=i;
if(i<=c[u]&&pa[i]) {
while(hd<=tl&&q[hd]>pa[i]) ++hd;
xa[i]=q[hd];
}
}
for(int i=2*c[u],hd=1,tl=0;i;--i) {
while(hd<=tl&&d[q[tl]]<=d[i]) --tl;
q[++tl]=i;
if(i<=c[u]&&pb[i]) {
while(hd<=tl&&q[hd]>pb[i]) ++hd;
xb[i]=q[hd];
}
}
for(int i=1;i<=c[u];++i) if(pa[i]) {
auto chk=[&](int lx,int rx,int ly,int ry) {
if(ly>ry) return 0;
if(ly>c[u]) ly-=c[u],ry-=c[u];
if(s[ry]-s[ly-1]!=B) return 0;
int dx=rx-lx+1,dy=ry-ly+1;
if(dx%2!=dy%2) return 0;
int k=(dx-dy)/2;
if(k>0) {
if(d[xb[ly]]<k) return 0;
for(int x=1;x<=c[u];++x) vis[b[x]]=1;
cout<<b[lx+ry-xb[ly]+k]<<" "<<in(b[xb[ly]],k)<<"\n";
return 1;
} else {
if(d[xa[lx]]<-k) return 0;
for(int x=1;x<=c[u];++x) vis[b[x]]=1;
cout<<in(b[xa[lx]],-k)<<" "<<b[ly+rx-xa[lx]-k]<<"\n";
return 1;
}
};
for(int l:{pa[i]+1,pa[i]+2}) for(int r:{i+c[u]-2,i+c[u]-1}) if(chk(i,pa[i],l,r)) return ;
}
auto chk=[&](int x,int up,int l,int r,int o) {
if(!l||!r) return 0;
if(r-l+1>=c[u]||r-l+1<(c[u]+1)/2) return 0;
int t=r-l+1-(c[u]+1)/2;
if(t>up) return 0;
for(int i=1;i<=c[u];++i) vis[b[i]]=1;
vis[x]=1;
int y=in(x,t),z=(o&2?b[r-t]:b[l+t]);
if(o&1) swap(y,z);
cout<<y<<" "<<z<<"\n";
return 1;
};
for(int i=1;i<=c[u];++i) for(int x:G[b[i]]) if(G[x].size()==1) {
int j=(i<c[u]?i+1:1),k=(i>1?i+c[u]-1:2*c[u]);
if(siz[x]==A&&chk(G[x][0],f[x]-1,j,pb[j],0)) return ;
if(siz[x]==B&&chk(G[x][0],f[x]-1,j,pa[j],1)) return ;
if(siz[x]==A&&chk(G[x][0],f[x]-1,pb[k],k,2)) return ;
if(siz[x]==B&&chk(G[x][0],f[x]-1,pa[k],k,3)) return ;
}
if(fa[fa[u]]&&G[fa[fa[u]]].size()==1) {
int x=fa[fa[u]];
if(n-siz[x]==A&&chk(fa[x],g[x],1,pb[1],0)) return ;
if(n-siz[x]==B&&chk(fa[x],g[x],1,pa[1],1)) return ;
if(n-siz[x]==A&&chk(fa[x],g[x],pb[2*c[u]-1],2*c[u]-1,2)) return ;
if(n-siz[x]==B&&chk(fa[x],g[x],pa[2*c[u]-1],2*c[u]-1,3)) return ;
}
}
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) {
solve(),dcnt=tp=0;
for(int i=0;i<=2*n;++i) {
G[i].clear(),E[i].clear(),H[i].clear();
dfn[i]=low[i]=st[i]=ins[i]=0;
siz[i]=fa[i]=c[i]=f[i]=g[i]=q[i]=w[i]=0;
d[i]=b[i]=s[i]=pa[i]=pb[i]=xa[i]=xb[i]=vis[i]=0;
}
}
return 0;
}
Z. [P14164] 命题作文 (3)
简单分讨后变成维护每条初始边的切边等价,那么我们要支持区间分裂并动态维护等价类。
对每条边 \(e\) 维护是否在每次询问中被覆盖,记为字符串 \(s_e\),则我们关心 \(s_e[1,i]\) 的本质不同子串类。
用主席树维护 \(s_e\) 并用二分哈希按字典序排序,然后倒序枚举 \(i\),并查集支持合并和维护等价类。
时间复杂度 \(\mathcal O(n\log^2n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
using namespace std;
const int MAXN=2.5e5+5;
mt19937_64 rnd(time(0));
int n,m,c1[MAXN],c2[MAXN],rt[MAXN],id[MAXN];
ull hv[MAXN];
struct Segt {
ull hs[MAXN*40];
int ls[MAXN*40],rs[MAXN*40],tot;
void init() {
for(int i=1;i<=tot;++i) hs[i]=ls[i]=rs[i]=0;
tot=0;
}
void ins(int x,int l,int r,int q,int &p) {
hs[p=++tot]=hs[q]^hv[x];
if(l==r) return ;
int mid=(l+r)>>1;
if(x<=mid) ins(x,l,mid,ls[q],ls[p]),rs[p]=rs[q];
else ins(x,mid+1,r,rs[q],rs[p]),ls[p]=ls[q];
}
bool cmp(int p,int q) {
if(hs[p]==hs[q]) return 0;
int l=1,r=m;
while(l<r) {
int mid=(l+r)>>1;
if(hs[ls[p]]==hs[ls[q]]) p=rs[p],q=rs[q],l=mid+1;
else p=ls[p],q=ls[q],r=mid;
}
return hs[p]<hs[q];
}
int lcp(int p,int q) {
if(hs[p]==hs[q]) return m;
int l=1,r=m;
while(l<r) {
int mid=(l+r)>>1;
if(hs[ls[p]]==hs[ls[q]]) p=rs[p],q=rs[q],l=mid+1;
else p=ls[p],q=ls[q],r=mid;
}
return l-1;
}
} T;
vector <int> X[MAXN];
vector <array<int,2>> E[MAXN];
int dsu[MAXN],siz[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
ll ans[MAXN],sum;
void solve() {
cin>>n>>m,T.init(),sum=0;
for(int i=1;i<=n;++i) X[i].clear();
for(int i=1;i<=m;++i) E[i].clear();
set <int> S1,S2;
S1.insert(n),S2.insert(n);
for(int i=1;i<n;++i) S1.insert(i);
for(int i=1,l,r;i<=m;++i) {
cin>>l>>r;
if(l>r) swap(l,r);
hv[i]=rnd(),X[l].push_back(i),X[r].push_back(i);
for(auto it=S2.lower_bound(l);*it<r;it=S2.erase(it));
for(auto it=S1.lower_bound(l);*it<r;it=S1.erase(it)) S2.insert(*it);
c1[i]=S1.size()-1,c2[i]=S2.size()-1;
}
for(int i=1;i<n;++i) {
rt[i]=rt[i-1],id[i]=i;
for(int x:X[i]) T.ins(x,1,m,rt[i],rt[i]);
}
stable_sort(id+1,id+n,[&](int x,int y){ return T.cmp(rt[x],rt[y]); });
for(int i=2;i<n;++i) E[T.lcp(rt[id[i-1]],rt[id[i]])].push_back({id[i-1],id[i]});
for(int i=1;i<n;++i) siz[i]=1,dsu[i]=i;
for(int i=m;i>=1;--i) {
for(auto e:E[i]) {
int x=find(e[0]),y=find(e[1]);
sum+=1ll*siz[x]*siz[y],dsu[y]=x,siz[x]+=siz[y];
}
ans[i]=sum+1ll*c1[i]*(n-1+i-c1[i])+c2[i];
}
for(int i=1;i<=m;++i) cout<<ans[i]<<"\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
*AA. [P13955] 延伸距离 (7)
首先该问题肯定需要用网络流建模,那么可以考虑把最短路转化为对偶图最小割。
那么建出对偶图,我们的操作就是增加一些边的容量使得最大流增加 \(k\)。
朴素建图就是连接 \((u,v,w,0),(u,v,\infty,1)\),然后求容量为 \(dis+k\) 的最小费用最大流。
可以先保留 \((u,v,w,0)\) 的边全部增广,然后连接 \((u,v,\infty,1)\) 求容量为 \(k\) 的最小费用最大流即可。
注意到第一部分的增广和权值无关,直接网络流即可。
时间复杂度 \(\mathcal O(n^3m^3+n^2m^2k)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9;
struct Edge { int v,e,f,w; } G[10005];
int S,T,ec=1,hd[505],dis[505],pre[505];
bool inq[505];
void adde(int u,int v,int f,int w) { G[++ec]={v,hd[u],f,w},hd[u]=ec; }
void link(int u,int v,int f,int w=0) { adde(u,v,f,w),adde(v,u,0,-w); }
bool SPFA() {
memset(dis,0x3f,sizeof(dis));
memset(pre,0,sizeof(pre));
memset(inq,false,sizeof(inq));
queue <int> Q; Q.push(S),inq[S]=true,dis[S]=0;
while(Q.size()) {
int u=Q.front(); Q.pop(),inq[u]=false;
for(int i=hd[u],v;i;i=G[i].e) if(G[i].f&&dis[v=G[i].v]>dis[u]+G[i].w) {
dis[v]=dis[u]+G[i].w,pre[v]=i;
if(!inq[v]) Q.push(v),inq[v]=true;
}
}
return pre[T];
}
array<int,2> ssp() {
int f=0,c=0;
while(SPFA()) {
int g=inf;
for(int u=T;u!=S;u=G[pre[u]^1].v) g=min(g,G[pre[u]].f);
f+=g,c+=g*dis[T];
for(int u=T;u!=S;u=G[pre[u]^1].v) G[pre[u]].f-=g,G[pre[u]^1].f+=g;
}
return {f,c};
}
int cur[505],dep[505];
bool BFS() {
memcpy(cur,hd,sizeof(cur)),memset(dep,-1,sizeof(dep));
queue <int> Q;
Q.push(S),dep[S]=0;
while(!Q.empty()) {
int u=Q.front(); Q.pop();
for(int i=hd[u];i;i=G[i].e) if(G[i].f&&dep[G[i].v]==-1) {
dep[G[i].v]=dep[u]+1,Q.push(G[i].v);
}
}
return ~dep[T];
}
int dfs(int u,int f) {
if(u==T) return f;
int r=f;
for(int i=cur[u];i;i=G[i].e) {
int v=G[cur[u]=i].v;
if(G[i].f&&dep[v]==dep[u]+1) {
int g=dfs(v,min(r,G[i].f));
if(!g) dep[v]=-1;
G[i].f-=g,G[i^1].f+=g,r-=g;
}
if(!r) return f;
}
return f-r;
}
int n,m,k,a[505][505],b[505][505],z1[505][505],z2[505][505];
int id(int x,int y) {
return x<1?S:(x<n?(x-1)*(m-1)+y:T);
}
void solve() {
memset(hd,0,sizeof(hd)),ec=1;
cin>>n>>m>>k,S=(n-1)*(m-1)+1,T=S+1;
for(int i=1;i<=n;++i) for(int j=1;j<m;++j) {
cin>>z1[i][j],link(id(i-1,j),id(i,j),z1[i][j]),link(id(i,j),id(i-1,j),z1[i][j]);
}
for(int i=1;i<n;++i) for(int j=1;j<=m;++j) {
cin>>z2[i][j];
if(1<j&&j<m) link(id(i,j-1),id(i,j),z2[i][j]),link(id(i,j),id(i,j-1),z2[i][j]);
}
int gg=0;
while(BFS()) gg+=dfs(S,inf);
for(int i=1;i<=n;++i) for(int j=1;j<m;++j) {
link(id(i-1,j),id(i,j),k,1),link(id(i,j),id(i-1,j),k,1),a[i][j]=ec;
}
for(int i=1;i<n;++i) for(int j=2;j<m;++j) {
link(id(i,j-1),id(i,j),k,1),link(id(i,j),id(i,j-1),k,1),b[i][j]=ec;
}
++T,link(T-1,T,k,0);
cout<<ssp()[1]<<"\n";
for(int i=1;i<=n;++i) for(int j=1;j<m;++j) cout<<z1[i][j]+G[a[i][j]].f+G[a[i][j]-2].f<<" \n"[j==m-1];
for(int i=1;i<n;++i) for(int j=1;j<=m;++j) cout<<z2[i][j]+(1<j&&j<m?G[b[i][j]].f+G[b[i][j]-2].f:0)<<" \n"[j==m];
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
AB. [P13960] 后缀结构 (3.5)
讨论 \(f(i,j)\) 是否 \(\ge j\),如果 \(f(i,j)\le j\),则取值就是 \(t[1,j]\) 在 \(s\) 的 AC 自动机上匹配到的节点。
否则我们要找某个 \(s_x=s_y+t[1,j]\),记 \(p_y\) 表示最大的 \(j\) 使得 \(t[1,j]\) 存在,则 \(y\) 就是 \(i\) 在 Fail 树上首个祖先满足 \(p_y\ge j\)。
求 \(p\) 可以直接二分哈希,求出 \(p\) 之后枚举 \(j\),动态维护每个 \(y\) 当前匹配了多少个 \(i\),用并查集维护即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
using namespace std;
const int MAXN=2e5+5,P=1e6+3;
struct Segt {
int tr[MAXN*20],ls[MAXN*20],rs[MAXN*20],tot;
void ins(int x,int c,int l,int r,int q,int &p) {
tr[p=++tot]=c;
if(l==r) return ;
int mid=(l+r)>>1;
if(x<=mid) ins(x,c,l,mid,ls[q],ls[p]),rs[p]=rs[q];
else ins(x,c,mid+1,r,rs[q],rs[p]),ls[p]=ls[q];
}
int qry(int x,int l,int r,int p) {
if(l==r) return tr[p];
int mid=(l+r)>>1;
return x<=mid?qry(x,l,mid,ls[p]):qry(x,mid+1,r,rs[p]);
}
void init() {
for(int i=1;i<=tot;++i) tr[i]=ls[i]=rs[i]=0;
tot=0;
}
} T;
int n,m,fa[MAXN],a[MAXN],b[MAXN],d[MAXN],nx[MAXN],hd[P];
ull B,pw[MAXN],hc[MAXN],ha[MAXN],hb[MAXN];
vector <int> G[MAXN],X[MAXN];
bool qry(ull z) {
for(int u=hd[z%P];u;u=nx[u]) if(ha[u]==z) return 1;
return 0;
}
int len[MAXN],rt[MAXN],up[MAXN],dsu[MAXN],siz[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
void solve() {
cin>>n>>m;
for(int i=1;i<=n;++i) cin>>fa[i];
for(int i=1;i<=n;++i) {
cin>>a[i],ha[i]=ha[fa[i]]*B+hc[a[i]],len[i]=len[fa[i]]+1;
G[fa[i]].push_back(i),nx[i]=i,swap(nx[i],hd[ha[i]%P]);
}
for(int i=1;i<=m;++i) cin>>b[i],hb[i]=hb[i-1]*B+hc[b[i]];
for(int i=1;i<=n;++i) {
for(int k=1<<19;k;k>>=1) if(d[i]+k<=m&&qry(ha[i]*pw[d[i]+k]+hb[d[i]+k])) d[i]+=k;
}
queue <int> Q;
for(int x:G[0]) T.ins(a[x],x,1,n,rt[0],rt[0]),Q.push(x);
while(Q.size()) {
int u=Q.front(); Q.pop(),rt[u]=rt[up[u]],X[d[u]].push_back(u);
for(int x:G[u]) up[x]=T.qry(a[x],1,n,rt[u]),T.ins(a[x],x,1,n,rt[u],rt[u]),Q.push(x);
}
ll sum=0;
for(int i=0;i<=n;++i) dsu[i]=i,siz[i]=i>0,sum+=len[i];
for(int i=1,o=0;i<=m;++i) {
o=T.qry(b[i],1,n,rt[o]);
for(int x:X[i-1]) {
int y=find(up[x]);
sum-=1ll*siz[x]*(len[x]-len[y]),dsu[x]=y,siz[y]+=siz[x];
}
cout<<sum+1ll*(n-siz[0])*i+1ll*siz[0]*len[o]<<" \n"[i==m];
}
T.init();
for(int i=0;i<=m;++i) X[i].clear();
for(int i=0;i<=n;++i) G[i].clear(),len[i]=d[i]=nx[i]=rt[i]=up[i]=0,hd[ha[i]%P]=0;
}
mt19937_64 rnd(time(0));
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
B=rnd()|1;
for(int i=pw[0]=1;i<MAXN;++i) pw[i]=pw[i-1]*B,hc[i]=rnd();
int _; cin>>_;
while(_--) solve();
return 0;
}

浙公网安备 33010602011771号