2025 NOI 做题记录(四)
\(\text{By DaiRuichen007}\)
Round #73 - 20250508
A. [P9600] Closing
题目大意
给定一棵 \(n\) 个点的树,每个点分配一个权值 \(x_i\),要求总和 \(\le k\)。
定义 \(f(u)\) 表示 \(u\) 为根,\(x_i\ge \mathrm{dist}(u,i)\) 的点构成的过 \(u\) 连通块大小。
给定 \(S,T\),分配权值最大化 \(f(S)+f(T)\)。
数据范围:\(n\le 2\times 10^5\)。
思路分析
用 \(a_i,b_i\) 表示到 \(S,T\) 的距离。
如果该成最大化 \(\sum [x_i\ge a_i]+[x_i\ge b_i]\),这个问题比较简单。
但我们此时求出的解可能有问题,\(x_i\ge a_i\) 但路径上前一个点不满足 \(x_j\ge a_j\)。
以 \(S\to T\) 路径为根,不在路径上的点有 \(a_u<a_{fa},b_u<b_{fa}\)。
因此如果 \(x_i\ge a_i,x_{fa}<a_{fa}\),交换后 \(x_i,x_{fa}\) 后更优。
从而这部分点贪心能求出正确答案,因此可能产生这种错误的一定是 \(S\to T\) 路径上的点
如果没有点贡献为 \(2\),直接按 \(\min(a_i,b_i)\) 依次取,显然此时取出的点一定是 \(S,T\) 为根的两个连通块。
否则有点贡献为 \(2\),显然一定有一个 \(S\to T\) 路径上的点贡献为 \(2\)。
那么从而 \(S\to T\) 上每个点贡献 \(\ge 1\),那么先令 \(x_i=\min(a_i,b_i)\),然后考虑贡献是否 \(+1\)。
可以证明此时最优解一定符合题意。
那么现在只要最大化 \(\sum [x_i\ge a_i]+[x_i\ge b_i]\),相当于每个物品可能产生 \(0,1,2\) 的收益,有些物品只能产生 \(0,1\) 的收益。
不妨设 \(a_i\le b_i\),如果 \(b_i-a_i\ge a_i\),那么直接拆成两个产生 \(0,1\) 收益的物品。
对于 \(b_i-a_i<a_i\) 的物品,显然至多一个物品产生 \(1\) 贡献,否则可以把一个换成 \(2\) 贡献一个换成 \(0\) 贡献。
那么枚举第二类物品中哪些有 \(2\) 贡献,显然是一个 \(b_i\) 的前缀,双指针求 \(0,1\) 收益的物品最多取几个。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
#include "closing.h"
#define ll long long
using namespace std;
const int MAXN=2e5+5;
const ll inf=1e18;
struct Edge { int v,w; };
vector <Edge> G[MAXN];
ll a[MAXN],b[MAXN];
void dfs(int u,int fz) {
for(auto e:G[u]) if(e.v^fz) a[e.v]=a[u]+e.w,dfs(e.v,u);
}
ll c[MAXN*2],mn[MAXN];
array<ll,2> d[MAXN];
int max_score(int n,int X,int Y,ll S,vector<int>U,vector<int>V,vector<int>W) {
for(int i=1;i<=n;++i) a[i]=b[i]=0,G[i].clear();
for(int i=0;i<n-1;++i) G[U[i]+1].push_back({V[i]+1,W[i]}),G[V[i]+1].push_back({U[i]+1,W[i]});
dfs(++X,0),swap(a,b),dfs(++Y,0);
for(int i=1;i<=n;++i) if(a[i]>b[i]) swap(a[i],b[i]);
memcpy(c,a,sizeof(c)),sort(c+1,c+n+1);
int ans=0; ll t=0;
while(ans<n&&t+c[ans+1]<=S) t+=c[++ans];
int m=0,q=0,ct=0;
for(int i=1;i<=n;++i) {
if(a[i]+b[i]==b[Y]) ++ct,S-=a[i],c[++m]=b[i]-a[i];
else if(b[i]-a[i]>=a[i]) c[++m]=a[i],c[++m]=b[i]-a[i];
else d[++q]={b[i],a[i]};
}
if(S<0) return ans;
sort(c+1,c+m+1),sort(d+1,d+q+1);
for(int i=1;i<=m;++i) c[i]+=c[i-1];
int p1=m,p2=m,p3=m;
auto ask=[&](ll z,int &p) -> int {
if(z<0) return -3*n;
while(c[p]>z) --p;
return p;
};
mn[q+1]=inf;
for(int i=q;i>=1;--i) mn[i]=min(mn[i+1],d[i][1]);
ans=max({ans,ask(S,p1)+ct,ask(S-mn[1],p3)+ct+1});
ll sum=0,vl=0;
for(int i=1;i<=q;++i) {
sum+=d[i][0],vl=max(vl,d[i][0]-d[i][1]);
ans=max({ans,ask(S-sum,p1)+ct+2*i,ask(S-sum+vl,p2)+ct+2*i-1,ask(S-sum-mn[i+1],p3)+ct+2*i+1});
}
return ans;
}
*B. [P9601] Longest Trip
题目大意
交互器有一张 \(n\) 个点的无向图,保证任意三个点之间至少有一条边。
你每次可以询问两个不交点集之间是否有边,\(400\) 次询问内构造出最长路。
数据范围:\(n\le 256\)。
思路分析
如果整张图不连通,那么这个图一定是两个完全图,答案就是这两个连通块的较大值。
如果整张图联通,我们不妨猜测整张图有哈密顿路。
我们考虑逐步在图中加点,保证图始终联通,并且在这个过程中动态维护哈密顿路。
考虑新加入点 \(u\) 和链的头尾 \(s,t\) 的关系,如果 \(u\to s,u\to t\) 存在任意一条即可,否则 \(s\to t\) 存在,那么哈密顿路实际上是一个环。
由于 \(u\) 与原图联通,我们找到 \(u\) 在环上的邻域 \(v\),连接 \(u\to v\) 然后切掉 \(v\) 的入边即可。
但这样构造过程需要太多次询问,考虑优化:
我们可以同时维护两条路径,每次加点 \(u\) 时取出两条路径的各一个端点 \(x,y\),如果 \(u\to x,u\to y\) 存在其一就可以直接加入,否则把 \(x,y\) 连接然后把 \(u\) 独立加入。
最后合并的时候看第一条路径的两个端点 \(a,b\) 和第二条路径的一个端点 \(c\),如果 \(a\to c,b\to c\) 存在其一直接合并,否则第一条路径是一个环,二分出 \(c\) 在其中的一个邻居即可。
这样的询问次数是 \(2n+\log n\) 的,需要进一步优化。
考虑 \(3\) 次操作加入两个点 \(u,v\),设两条路径各一个端点 \(x,y\),先询问 \(u\to v\) 是否存在:
-
如果 \(u\to v\) 不存在,那么询问 \(u\to x,u\to y\) 是否存在:
- 如果两条边都存在那么就把两条链用 \(u\) 连起来,\(v\) 单独成链。
- 如果恰存在一条边,那么另一个端点一定和 \(v\) 联通,分别加入一条链即可。
- 如果两条边都不存在,那么原来的两条链用 \(v\) 连起来,\(u\) 单独成链。
-
如果 \(u\to v\) 存在,那么询问 \(u\to x,u\to y\) 是否存在:
- 如果存在至少一条,那么把 \(u,v\) 都加入该链末尾。
- 否则说明 \(x\to y\) 存在,把原来的两条链连成一条,\(u\to v\) 成为新的一条链。
操作次数大约为 \(1.5n+2\log n\),可以计算得到最大次数恰好为 \(400\)。
代码呈现
#include<bits/stdc++.h>
#include "longesttrip.h"
using namespace std;
bool qry(int x,int y) { return are_connected({x},{y}); }
vector<int>longest_trip(int n,int d) {
vector <int> a{0},b{1};
for(int i=2;i+1<n;i+=2) {
int x=a.back(),y=b.back();
if(qry(i,i+1)) {
if(qry(i,x)) a.push_back(i),a.push_back(i+1);
else if(qry(i,y)) b.push_back(i),b.push_back(i+1);
else {
for(int j=b.size()-1;~j;--j) a.push_back(b[j]);
b={i,i+1};
}
} else {
bool cx=qry(i,x),cy=qry(i,y);
if(cx&&cy) {
a.push_back(i);
for(int j=b.size()-1;~j;--j) a.push_back(b[j]);
b={i+1};
} else if(cx||cy) {
a.push_back(i+cy),b.push_back(i+cx);
} else {
b.push_back(i+1);
for(int j=a.size()-1;~j;--j) b.push_back(a[j]);
a={i};
}
}
}
if(n&1) {
int x=a.back(),y=b.back();
if(qry(n-1,x)) a.push_back(n-1);
else if(qry(n-1,y)) b.push_back(n-1);
else {
for(int i=b.size()-1;~i;--i) a.push_back(b[i]);
b={n-1};
}
}
if(a.size()>b.size()) swap(a,b);
if(!are_connected(a,b)) return b;
int x=a.back();
if(qry(x,b.front())) {
for(int i:b) a.push_back(i);
return a;
}
if(qry(x,b.back())) {
for(int i=b.size()-1;~i;--i) a.push_back(b[i]);
return a;
}
if(qry(a[0],b.back())) {
for(int i:a) b.push_back(i);
return b;
}
int l=0,r=(int)a.size()-1;
while(l<r) {
int mid=(l+r)>>1;
if(are_connected(vector<int>(a.begin()+l,a.begin()+mid+1),b)) r=mid;
else l=mid+1;
}
if(l!=(int)a.size()-1) rotate(a.begin(),a.begin()+l+1,a.end());
x=a.back(),l=0,r=(int)b.size()-1;
while(l<r) {
int mid=(l+r)>>1;
if(are_connected(vector<int>(b.begin()+l,b.begin()+mid+1),{x})) r=mid;
else l=mid+1;
}
rotate(b.begin(),b.begin()+l,b.end());
for(int i:b) a.push_back(i);
return a;
}
*C. [P9602] Soccer
题目大意
给定 \(n\times n\) 矩阵,有些位置不能选择,选出最大的子集,使得任意两个格子可以通过 \(\le 1\) 次转弯互相到达。
数据范围:\(n\le 2000\)。
思路分析
观察一下可能的点集,容易发现任意两行都有包含关系,并且每行的大小是单峰的。
那么我们可以用 \(f_{l,r,u,d}\) 表示 \([l,r]\) 行,最窄的一行为 \([u,d]\)。
转移时加入 \(l-1/r+1\) 行,转移到 \([u,d]\) 范围内某个连续的可选择的区间。
注意到 \([u,d]\) 可以用任意 \(x\in [u,d]\) 描述,此时的 \([u,d]\) 就是 \([l,r]\) 的每一行中 \(x\) 所在连续段的交。
进一步可以把 \(l\) 也优化掉,即有意义的 \(l\) 一定是 \((r,x)\) 所在的列连续段的首行。
枚举 \((r,x)\),算出 \([u,d]\),如果向下转移,直接从 \(f_{r-1,x}+d-u+1\) 转移。
如果向上转移,那么 \([u,d]\) 在第 \(l\) 行,那么找到一个包含 \([u,d]\) 的状态,那么就是从 \(f_{r,u-1}\) 或 \(f_{r,d+1}\) 转移,算出哪些行的宽度为 \([u,d]\) 并转移即可。
预处理每个 \((r,x)\) 对应的 \(r-l\),按此排序处理所有状态即可。
时间复杂度 \(\mathcal O(n^2)\)。
代码呈现
#include<bits/stdc++.h>
#include "soccer.h"
using namespace std;
const int MAXN=2005;
int L[MAXN][MAXN],R[MAXN][MAXN],h[MAXN][MAXN],f[MAXN][MAXN];
vector <array<int,2>> o[MAXN];
int biggest_stadium(int n,vector<vector<int>>a) {
for(int i=1;i<=n;++i) {
for(int j=1;j<=n;++j) {
if(a[i-1][j-1]) h[i][j]=i,L[i][j]=R[i][j]=j;
else h[i][j]=h[i-1][j],L[i][j]=L[i][j-1],o[i-h[i][j]].push_back({i,j});
}
R[i][n+1]=n+1;
for(int j=n;j;--j) if(!R[i][j]) R[i][j]=R[i][j+1];
}
int ans=0;
for(int e=1;e<=n;++e) for(auto u:o[e]) {
int i=u[0],j=u[1];
if(e>1) L[i][j]=max(L[i-1][j],L[i][j]),R[i][j]=min(R[i-1][j],R[i][j]);
int d=R[i][j]-L[i][j]-1;
f[i][j]=f[i-1][j]+d;
if(L[i][j]>=1) f[i][j]=max(f[i][j],f[i][L[i][j]]+d*(h[i][L[i][j]]-h[i][j]));
if(R[i][j]<=n) f[i][j]=max(f[i][j],f[i][R[i][j]]+d*(h[i][R[i][j]]-h[i][j]));
ans=max(ans,f[i][j]);
}
return ans;
}
*D. [P9603] Beech Tree
题目大意
给定一棵 \(n\) 个点的树,每个点有颜色。
定义一个子树是好的,当且仅当存在一个节点的排列 \(p\),使得:
- \(p_0\) 为根
- \(p_i\) 的父亲为 \(p_c\),其中 \(c\) 是 \(p[1,i)\) 中颜色和 \(p_i\) 相同的节点个数。
求哪些子树是好的。
数据范围:\(n\le 2\times 10^5\)。
思路分析
手玩一下构造过程,首先 \(p_0\) 的儿子一定是同色点中的首个出现,\(p_1\) 的儿子一定是同色点中的第二个出现。
那么 \(p_1\) 的儿子颜色集合一定是 \(p_0\) 儿子颜色集合的子集,以此类推。
因此一棵树是好的必要条件是任意两个点儿子颜色集合有包含关系
但这个条件并不充分,因为对于 \(p_0,p_1\) 的两个同色儿子 \(u,v\),\(u\) 在排列中必须在 \(v\) 前面,从而 \(u\) 儿子颜色集合必须包含 \(v\) 儿子颜色集合。
那么不断递归,我们得到 \(p_i\) 的子树一定包含 \(p_{i+1}\) 子树,即存在一个子图等于 \(p_{i+1}\) 子树。
那么我们把所有点的子树按 \(\mathrm{siz}\) 排序,判断相邻两个点是否有包含关系,启发式合并维护。
注意到每次判断不需要比较子树,只要比较根的每种颜色的儿子的 \(\mathrm{siz}\),因为对应子树之间的包含关系已经检验过了。
时间复杂度 \(\mathcal O(n\log^2n)\)。
代码呈现
#include<bits/stdc++.h>
#include "beechtree.h"
#define fi first
#define se second
using namespace std;
const int MAXN=2e5+5;
int siz[MAXN];
map <int,int> G[MAXN];
set <pair<int,int>> f[MAXN];
bool ans[MAXN];
bool chk(int x,int y) {
for(auto e:G[x]) if(!G[y].count(e.fi)||siz[e.se]>siz[G[y][e.fi]]) return false;
return true;
}
bool merge(set<pair<int,int>>&g,set<pair<int,int>>&h) {
if(g.size()<h.size()) g.swap(h);
for(auto o:h) {
auto it=g.insert(o).fi;
if(it!=g.begin()&&!chk(prev(it)->se,o.se)) return false;
if(next(it)!=g.end()&&!chk(o.se,next(it)->se)) return false;
}
return true;
}
void dfs(int u) {
siz[u]=1;
for(auto e:G[u]) dfs(e.se),ans[u]&=ans[e.se],siz[u]+=siz[e.se];
if(!ans[u]) return ;
f[u].insert({siz[u],u});
for(auto e:G[u]) {
if(!merge(f[u],f[e.se])) return ans[u]=false,void();
}
}
vector<int> beechtree(int n,int m,vector<int>P,vector<int>c) {
for(int i=0;i<n;++i) ans[i]=true;
for(int i=1;i<n;++i) {
if(G[P[i]].count(c[i])) ans[P[i]]=false;
G[P[i]][c[i]]=i;
}
for(int i=0;i<n;++i) if(!siz[i]) dfs(i);
return vector<int>(ans,ans+n);
}
E. [P9604] Overtaking
题目大意
给定 \(n\) 辆车,第 \(i\) 辆的速度为 \(w_i\) 秒每公里,在 \(t_i\) 时刻出发。
路径上有 \(m\) 个检查点,\(s_0\sim s_{m-1}\),设第 \(i\) 个车到达 \(s_j\) 的时间为 \(f_{i,j}\),则 \(f_{i,j}=\max_k\{f_{i-1,k}+w_k(s_i-s_{i-1})\mid f_{i-1,k}<f_{i-1,j}\lor k=j\}\)。
\(q\) 次询问 \(y_1\sim y_q\),求出加入一辆速度为 \(x\),在 \(y_i\) 出发的车,何时到达 \(s_{m-1}\)。
数据范围:\(n,m\le 1000,q\le 2\times 10^5\).
思路分析
注意到速度比 \(x\) 快的车不可能卡住询问车,因此这些车可以删除。
先预处理出 \(f_{i,j}\),然后可以 \(\mathcal O(m\log n)\) 模拟每个车的过程。
进一步,我们只在每个点被其他车卡住的时候特殊处理,可以 \(\mathcal O(\log m)\) 二分出被卡住的某个时刻。
那么用 \(w(i,j)\) 表示 \(j\) 时刻到达 \(s_i\),何时到达 \(s_{m-1}\),注意到每次二分后一定形如求某个 \(w(i,f_{i,j})\),从而状态总数只有 \(\mathcal O(q+nm)\) 个,记忆化搜索即可。
时间复杂度 \(\mathcal O((q+nm)(\log n+\log m))\)。
代码呈现
#include<bits/stdc++.h>
#include "overtaking.h"
#define ll long long
using namespace std;
const int MAXN=1005;
int n,m,id[MAXN];
ll d[MAXN],v[MAXN],x,a[MAXN][MAXN],b[MAXN];
unordered_map <ll,ll> f[MAXN];
ll qry(int i,ll p) {
if(i==m-1) return p;
if(f[i].count(p)) return f[i][p];
int l=i+1,r=m-1,t=m,o=lower_bound(a[i],a[i]+n,p)-a[i]-1;
while(l<=r) {
int mid=(l+r)>>1;
if(a[mid][o]>=p+(d[mid]-d[i])*x) t=mid,r=mid-1;
else l=mid+1;
}
if(t==m) return f[i][p]=p+(d[m-1]-d[i])*x;
return f[i][p]=qry(t,a[t][o]);
}
void init(int L,int N,vector<ll>T,vector<int>W,int X,int M,vector<int>S) {
m=M,x=X;
for(int i=0;i<N;++i) if(W[i]>X) v[n]=W[i],b[n++]=T[i];
for(int i=0;i<m;++i) d[i]=S[i];
for(int t=0;t<m;++t) {
for(int i=0;i<n;++i) id[i]=i;
sort(id,id+n,[&](int i,int j){ return b[i]^b[j]?b[i]<b[j]:v[i]<v[j]; });
for(int i=0;i<n;++i) a[t][i]=b[id[i]];
if(t==m-1) break;
ll mx=0;
for(int i=0;i<n;++i) b[id[i]]=mx=max(mx,b[id[i]]+(d[t+1]-d[t])*v[id[i]]);
}
}
ll arrival_time(ll Y) { return qry(0,Y); }
*F. [P9605] Robot
题目大意
你需要设计一个机器人,使得其能在一个 \(n\times m\) 迷宫中找到左上到右下最短路。
你的机器人可以在每个位置上留下 \([0,6]\) 之间的标记,并且收集当前位置以及四联通位置的标记,然后留在原地或者向四个方向之一移动一步。
构造一个策略,使得机器人在 \(5\times 10^5\),可以把最短路上的点标 \(0\),其他标 \(1\)。
数据范围:\(n,m\le 15\)。
思路分析
实际上可以做到 \(Z\in\{0,1,2,3,4,5\}\)。
我们把 \(2,3,4,5\) 分别用来表示上下左右的方向。
第一部分,先通过 bfs 求出一条最短路。
我们只能用类似 dfs 的过程维护 bfs,即每次 dfs 拓展一轮叶子。
首先我们要判断当前是否是起点,当且仅当 U,L 方向是边界且当前是 \(0\),那么就把这个格子指向 D(如果是墙就指向 R)并移动。
当我们到达一个新格子(标号为 \(0\))时,我们将指针指向其上一步走到的格子并返回(找唯一一个指向自己的格子)。
如果该格子已经访问,就将其指向的方向逆时针旋转,如果如果有未访问的点指向其并走过去,如果没有那么就会转回父亲,指向父亲并返回即可。
容易发现每次重新走到根,都会让每个点的指针从父亲逆时针旋转一周再回到父亲,那么每个点就会在 bfs 的过程中推进一轮。
如果我们走到了右下角,那么就进入了第二部分。
我们需要将 bfs 树上路径的的点标 \(1\) 并把其他点标 \(0\)。
那么此时所有指针形成了一棵以右下角为根的内向树。
在这一阶段下我们不改变指针的结构,直接把每个点标成 \(0/1\)。
进行 dfs,每当我们到达一个节点时,如果有指向其的邻居,那么先走过去(任选一个)。
如果该节点的邻居都已解决,那么我们进行判断:如果该节点在最短路径上,当且仅当其为左上角或者在树上有标为 \(1\) 的邻居,然后把这个点标为 \(0/1\)。
那么此时的 dfs 树按后序遍历确定了若干点,且已确定的点标号为 \(0/1\),这是容易判断的。
那么可以在 \(\mathcal O(n^2m^2)\) 的步数内完成整个过程。
代码呈现
#include<bits/stdc++.h>
#include "robot.h"
using namespace std;
const char op[]=" WSEN";
int a[6];
void sol() {
auto ans=[&](int z,char c) { set_instruction({a[1],a[2],a[3],a[4],a[5]},z,c); };
int o=a[1];
if(!o) {
if(a[2]==-2&&a[5]==-2) return ~a[3]?ans(3,'S'):ans(4,'E');
if(a[3]==-2&&a[4]==-2) return ans(4,'H');
for(int i:{2,3,4,5}) if(a[i]==i%4+2) return ans(i,op[i]);
return ;
}
if(o>5||o<2) return ;
if(a[o]==o%4+2) {
for(int c:{1,2,3}) {
int i=(o-2+c)%4+2;
if(!a[i]||a[i]==i%4+2) return ans(i,op[i]);
}
return ans(o,op[o]);
}
bool ok=0;
if(a[2]==-2&&a[5]==-2) ok=1;
for(int i:{2,3,4,5}) if(a[i]==i%4+2) return ans(o,op[i]);
for(int i:{2,3,4,5}) ok|=a[i]==1;
if(a[3]==-2&&a[4]==-2) return ans(1,'T');
return ans(ok,op[o]);
}
void program_pulibot() {
for(a[1]=-2;a[1]<=5;++a[1]) for(a[2]=-2;a[2]<=5;++a[2]) for(a[3]=-2;a[3]<=5;++a[3]) for(a[4]=-2;a[4]<=5;++a[4]) for(a[5]=-2;a[5]<=5;++a[5]) sol();
}
Round #74 - 20250514
*A. [P8518] Candies
题目大意
序列 \(a_1\sim a_n\),初始全为零,进行 \(q\) 次区间加,每次操作后 \(a_i\gets \max(0,\min(a_i,c_i))\),求最终的 \(a\)。
数据范围:\(n\le 2\times 10^5\)。
思路分析
首先可以想到对 \(i\) 扫描线,对时间轴建立线段树。
那我们就要找到最后一个 \(a_i<0\) 或 \(a_i>c\) 的时刻。
如果 \(c_i=\infty\),则最后一个时刻一定是最小前缀和的位置。
否则如果碰到先后过上界和下界,找到最小的后缀 \([p,q]\) 使得这个范围内的最大子段和 \(>c\) 或最小子段和 \(<-c\)。
那么操作 \(p\) 之后,\([p+1,q]\) 的部分只会碰到上界或下界中的一个,用上面的方法维护总和以及最大最小前缀和即可。
线段树上二分维护该过程。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
#include "candies.h"
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,q;
struct info {
ll mn,mx,su;
inline friend info operator +(const info &u,const info &v) {
return {min(u.mn,u.su+v.mn),max(u.mx,u.su+v.mx),u.su+v.su};
}
};
struct SegmentTree {
info tr[1<<19];
void psu(int p) { tr[p]=tr[p<<1]+tr[p<<1|1]; }
void set(int u,int x,int l=0,int r=q,int p=1) {
if(l==r) return tr[p]=info{min(0,x),max(0,x),x},void();
int mid=(l+r)>>1;
u<=mid?set(u,x,l,mid,p<<1):set(u,x,mid+1,r,p<<1|1);
psu(p);
}
int qry(int c,info o,int l=0,int r=q,int p=1) {
if(l==r) {
if(tr[p].su<=0) return o.mx>c?c+o.su-o.mx:o.su-o.mn;
else return o.mn<-c?o.su-o.mn:c+o.su-o.mx;
}
int mid=(l+r)>>1; info t=tr[p<<1|1]+o;
if(t.mx-t.mn>c) return qry(c,o,mid+1,r,p<<1|1);
else return qry(c,t,l,mid,p<<1);
}
} T;
vector <array<int,2>> op[MAXN];
vector<int> distribute_candies(vector<int>c,vector<int>l,vector<int>r,vector<int>v) {
n=c.size(),q=l.size();
for(int i=0;i<q;++i) op[l[i]].push_back({i+1,v[i]}),op[r[i]+1].push_back({i+1,0});
vector <int> ans(n);
for(int i=0;i<n;++i) {
for(auto o:op[i]) T.set(o[0],o[1]);
ans[i]=T.qry(c[i],{0,0,0});
}
return ans;
}
*B. [P8519] Keys
题目大意
给定 \(n\) 个点 \(m\) 条边的无向图,点和边都有颜色,一条边可以经过当且仅当已经访问过某个和该边同色的点,求从哪些点出发能到达的点数最少。
数据范围:\(n,m\le 3\times 10^5\)。
思路分析
如果 \(u\) 出发能到 \(v\) 就连边 \(u\to v\),注意到 \(u\to v,v\to w\) 都存在则 \(u\to w\) 显然存在。
因此我们相当于求有向图上哪些点后继数量最少,缩点后只要考虑所有出度 \(=0\) 的强连通分量。
因此如果有边 \(u\to v\),则 \(u\) 的答案大于等于 \(v\) 的答案。
进一步可以考虑任取一个极大的内向树生成森林,答案只可能是所有根节点所在的强连通分量,并且只要求出每个根节点的后继即可。
那么类似 Boruvka,每次给每个连通块的根找一条出边,此时每条边至多访问一次,且连通块数量减半。
时间复杂度 \(\mathcal O(m\log n)\)。
代码呈现
#include<bits/stdc++.h>
#include "keys.h"
using namespace std;
const int MAXN=3e5+5;
struct Edge { int v,w; };
vector <Edge> G[MAXN];
int st[MAXN],dsu[MAXN];
bool vis[MAXN],inq[MAXN];
vector <int> id[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
vector<int> find_reachable(vector<int>a,vector<int>U,vector<int>V,vector<int>c) {
int n=a.size(); iota(dsu,dsu+n,0);
for(int i=0;i<(int)c.size();++i) G[U[i]].push_back({V[i],c[i]}),G[V[i]].push_back({U[i],c[i]});
int ans=n+1; vector <int> res,z(n);
while(true) {
vector <array<int,2>> E;
for(int o=0;o<n;++o) if(dsu[o]==o) {
int tp=0; st[++tp]=o,inq[a[o]]=true,vis[o]=true;
for(int i=1;i<=tp;++i) {
int x=st[i];
function<bool(int)> add=[&](int v) {
if(dsu[v]!=dsu[o]) return E.push_back({dsu[o],dsu[v]}),true;
if(!vis[v]) {
st[++tp]=v,vis[v]=true;
if(!inq[a[v]]) {
inq[a[v]]=true;
for(int u:id[a[v]]) if(add(u)) return true;
id[a[v]].clear();
}
}
return false;
};
for(auto e:G[x]) {
if(inq[e.w]) {
if(add(e.v)) goto fi;
} else id[e.w].push_back(e.v);
}
}
if(tp<ans) ans=tp,res.clear();
if(ans==tp) for(int i=1;i<=tp;++i) res.push_back(st[i]);
fi:;
for(int i=1;i<=tp;++i) {
vis[st[i]]=inq[a[st[i]]]=false;
for(auto e:G[st[i]]) id[e.w].clear();
}
}
if(E.empty()) break;
for(auto e:E) dsu[find(e[0])]=find(e[1]);
for(int i=0;i<n;++i) dsu[i]=find(i);
}
for(int i:res) z[i]=true;
return z;
}
*C. [P8520] Parks
题目大意
给定网格上的 \(n\) 个格点,求这些点的一棵生成树,并给每条边匹配一个相邻的方格,要求每个方格至多匹配一条边,构造方案。
数据范围:\(n\le 2\times 10^5\)。
思路分析
考虑没有方格的四个顶点都在点集内的的情况,则生成树唯一。
一个想法是把方格黑白染色,黑格子只匹配竖边,白格子只匹配横边,此时每条边恰有唯一可能的格子。
由于每个方格周围四个顶点不全被选,因此直接匹配不会冲突。
对于一般的情况,如果一个黑格子左右都有竖边,那么把一条竖边换成一条原先没有的横边即可。
为了让加入的横边不继续导出矛盾,我们从下到上从左到右考虑每条边,始终保证尽量连通。
如果 \((x-2,y-2),(x-2,y)\) 以及 \((x,y-2),(x,y)\) 同时存在,那么删掉 \((x,y-2),(x,y)\) 加入 \((x-2,y),(x,y)\)。
如果 \((x-2,y),(x,y)\) 以及 \((x-2,y-2),(x,y-2)\) 同时存在,说明此前 \((x-2,y),(x,y)\) 不连通,但这两条竖边刚刚一定被加入了,因此矛盾。
因此从下到上构造,贪心加入每条边一定合法。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
#include "parks.h"
using namespace std;
const int MAXN=2e5+5;
vector <array<int,2>> p[MAXN];
unordered_map <int,int> id[MAXN],vis[MAXN];
int dsu[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
int construct_roads(vector<int>X,vector<int>Y) {
int n=X.size(); iota(dsu,dsu+n,0);
for(int i=0;i<n;++i) p[Y[i]].push_back({X[i],i}),id[X[i]][Y[i]]=i;
vector <int> wu,wv,wa,wb;
auto link=[&](int u,int v,int x,int y) {
wu.push_back(u),wv.push_back(v),wa.push_back(x),wb.push_back(y);
dsu[find(u)]=find(v),vis[x][y]=1;
};
for(int y=0;y<MAXN;y+=2) {
sort(p[y].begin(),p[y].end());
for(auto o:p[y]) {
int x=o[0],u=o[1];
if(!id[x].count(y-2)) continue;
int v=id[x][y-2];
if(find(u)==find(v)) continue;
if((x/2+(y-2)/2)&1) link(u,v,x+1,y-1);
else if(!vis[x-1].count(y-1)) link(u,v,x-1,y-1);
else link(u,id[x-2][y],x-1,y+1);
}
for(auto o:p[y]) {
int x=o[0],u=o[1];
if(!id[x-2].count(y)) continue;
int v=id[x-2][y];
if(find(u)==find(v)) continue;
if(~((x-2)/2+y/2)&1) link(u,v,x-1,y+1);
else if(!vis[x-1].count(y-1)) link(u,v,x-1,y-1);
else assert(0);
}
}
for(int i=1;i<n;++i) if(find(i)!=find(0)) return 0;
build(wu,wv,wa,wb);
return 1;
}
D. [P8521] Dna
题目大意
给定字符集大小为 \(3\) 的字符串 \(s,t\),\(q\) 次询问 \(s[l,r]\) 至少几次交换才能变成 \(t[l,r]\)。
数据范围:\(n,q\le 10^5\)。
思路分析
考虑 \(s_i\) 最终到了哪个 \(t_j\) 上,\(i\to j\) 连边,答案为长度减去环数。
因此我们要最大化环数,把每个字符看成点,就变成在 \(3\) 个点的图上分解出尽可能多的环。
先贪心取长度为 \(2\) 的环,再取长度为 \(3\) 的环即可。
时间复杂度 \(\mathcal O(n+q)\)。
代码呈现
#include<bits/stdc++.h>
#include "dna.h"
using namespace std;
const int MAXN=1e5+5;
int f[MAXN][6];
void init(string a,string b) {
int n=a.size(); a=" "+a,b=" "+b;
for(int i=1;i<=n;++i) {
memcpy(f[i],f[i-1],sizeof(f[i]));
if(a[i]=='A'&&b[i]=='T') ++f[i][0];
if(a[i]=='T'&&b[i]=='A') ++f[i][1];
if(a[i]=='T'&&b[i]=='C') ++f[i][2];
if(a[i]=='C'&&b[i]=='T') ++f[i][3];
if(a[i]=='C'&&b[i]=='A') ++f[i][4];
if(a[i]=='A'&&b[i]=='C') ++f[i][5];
}
}
int get_distance(int x,int y) {
int c[6],s=0,k;
for(int i=0;i<6;++i) c[i]=f[y+1][i]-f[x][i];
for(int d:{0,2,4}) k=min(c[d],c[d^1]),s+=k,c[d]-=k,c[d^1]-=k;
k=min({c[0],c[2],c[4]}),s+=2*k,c[0]-=k,c[2]-=k,c[4]-=k;
k=min({c[1],c[3],c[5]}),s+=2*k,c[1]-=k,c[3]-=k,c[5]-=k;
if(*max_element(c,c+6)) return -1;
return s;
}
*E. [P8522] Dungeons
题目大意
你要挑战 \(n\) 个人,假设当前对手为 \(i\),如果你当前的实力 \(\ge s_i\),则实力加上 \(s_i\),接下来挑战 \(w_i\),否则实力加上 \(p_i\),接下来挑战 \(l_i\)。
\(q\) 次询问 \(x\) 出发,初始实力为 \(z\) 时几次操作挑战 \(n+1\)。
数据范围:\(n\le 4\times 10^5,q\le 5\times 10^4,s_i,p_i\le 10^7\)。
思路分析
倍增值域分块,设当前 \(z\in[2^k,2^{k+1})\),考虑何时 \(z\ge 2^{k+1}\),那么我们只要找到第一个 \(s_i\ge 2^k\) 且 \(s_i\le z\) 的点即可。
不妨钦定每次都有 \(z<s_i\),那么获胜当且仅当 \(s_i<2^k\),可以直接预处理得到从 \(x\) 出发走 \(2^d\) 步,如果想要输给所有 \(s_i\ge 2^k\) 的点,那么 \(z\) 至多是多少。
然后倍增一下就找到了第一个 \(s_i\in[2^k,z]\) 的点。
但是空间复杂度太大,把块长放成 \([B^k,B^{k+1})\),那么 \(B\) 次赢某些 \(s_i\) 后就跳出这个块了,取 \(B=16\) 课题通过。
时间复杂度 \(\mathcal O(n\log_B V\log n+qB\log_B V\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
#include "dungeons.h"
using namespace std;
const int MAXN=4e5+5,pw[]={1,16,256,4096,65536,1048576,16777216};
const ll inf=1e18;
int n,s[MAXN],w[MAXN],l[MAXN],p[MAXN];
struct info {
int u; ll lim,sum;
friend info operator +(info x,info y) {
return {y.u,min(x.lim,y.lim-x.sum),x.sum+y.sum};
}
} f[7][25][MAXN];
void init(int N,vector <int> S,vector <int> P,vector <int> W,vector <int> L) {
n=N;
for(int i=0;i<n;++i) s[i]=S[i],p[i]=P[i],w[i]=W[i],l[i]=L[i];
for(int d=0;d<7;++d) {
for(int i=0;i<n;++i) f[d][0][i]=(s[i]<=pw[d])?info{w[i],inf,s[i]}:info{l[i],s[i],p[i]};
for(int k=1;k<25;++k) {
info *g=f[d][k-1],*h=f[d][k];
for(int i=0;i<n;++i) h[i]=(g[i].u==n)?g[i]:g[i]+g[g[i].u];
}
}
}
ll simulate(int x,int Z) {
ll z=Z;
while(x!=n) {
int d=0;
while(d<6&&pw[d+1]<=z) ++d;
for(int k=24;~k;--k) {
info e=f[d][k][x];
if(e.u!=n&&z<e.lim) x=e.u,z+=e.sum;
}
z>=s[x]?(z+=s[x],x=w[x]):(z+=p[x],x=l[x]);
}
return z;
}
*F. [P8523] Registers
题目大意
给定 \(100\) 个 \(2000\) 位二进制变量,每次操作可以是赋值、拷贝、左移、右移、取与、取或、取异或、取反、加法,构造一个程序完成如下任务:
- \(150\) 次操作内,求出 \(n\) 个 \(k\) 位二进制数的最小值。
- \(4000\) 次操作内,给 \(n\) 个 \(k\) 位二进制数排序。
输入输出格式:所有数依次存储在 \(0\) 号变量的 $[0,k),[k,2k),[2k,3k),\dots $ 位。
数据范围:\(n\le 100,k\le 10\)。
思路分析
在没有判断语句的时候很难比较两个数的大小。
一种想法是加上符号位,然后比较 \(a+b\) 的大小,即 \(a+(2^{k+1}-1-b)\bmod 2^{k+1}\) 的 \(2^k\) 位为 \(0\) 说明 \(a>b\),否则 \(a\le b\)。
然后与上 \(2^{k}\),得到 \(s_k\) 就是 \([a\le b]\),那么 \(s\) 或上 \(s\) 左移 \(1,2,4,8\) 位,则 \(s=[a\le b]\times(2^{k+1}-1)\)。
因此 \(\min(a,b)=b\oplus (s\operatorname{AND} (a\oplus b))\)。
回到原问题,由于所有的位是连续给出,因此没有放符号位的空间。
我们可以特判第 \(k\) 位的大小关系,然后把第 \(k\) 位当符号位,比较前 \(2^{k-1}\) 位的结果。
那么真正的 \(s\) 就是 \(s'\operatorname{AND}(\mathrm{NOT}(a\oplus b))\) 再或上 \((a\oplus b)\operatorname{OR} b\) 的第 \(k\) 位。
注意到 \(b\) 为 \(a\) 左移 \(k\) 的时候可以一次性让每个 \(a_i\gets \min(a_i,a_{i+1})\),然后 \(b\) 取 \(a\) 左移 \(2k\),\(a_i\gets \min(a_i,a_{i+1},a_{i+2},a_{i+3})\),只要进行 \(\lceil \log_2n\rceil\) 轮就能得到答案。
注意这里 \(s_k\to s\) 的过程中不能左移 \(1,2,4,8\) 位,否则可能影响到前一个 \(s\)。
并且前一个 \(a+(2^k-1-b)\) 可能对下一个有进位,但 \(a\gets a+1\) 后 \(s_k\) 变化当且仅当 \(a=b\),那么这种情况 \(s_k\in\{0,1\}\) 都等价。
可以构造 \((13+2\lceil\log_2k\rceil)\lceil\log_2n\rceil+2\le 149\) 次操作的方案。
然后是第二问,可以根据 \(s\) 求出 \(\min(a_i,a_{i+1}),\max(a_i,a_{i+1})\),然后奇偶排序即可。
即第 \(t\) 轮对 \(i\bmod 2=t\bmod 2\) 的点冒泡排序,这样至多 \(n\) 次就能还原。
操作次数不超过 \((22+2\lceil\log_2k\rceil)n+6\le 3006\)。
代码呈现
#include<bits/stdc++.h>
#include "registers.h"
using namespace std;
namespace luotianyi {
const int M=100,B=2000,hi1=99,hi0=98;
int n,k;
const int a=0,b=1,XOR=2,SGN=3,ta=4,tb=5,DIF=6,NXOR=7,tmp=8;
void cmp(int d) {
append_right(b,a,d*k);
append_xor(XOR,a,b);
append_and(SGN,XOR,b);
append_and(ta,a,hi0);
append_and(tb,b,hi0);
append_not(tb,tb);
append_add(DIF,ta,tb);
append_not(NXOR,XOR);
append_and(DIF,DIF,NXOR);
append_or(SGN,SGN,DIF);
append_and(SGN,SGN,hi1);
for(int i=1;i<k;i*=2) {
append_right(tmp,SGN,min(i,k-i));
append_or(SGN,SGN,tmp);
}
}
const int odd=97,even=96,mn=9,mx=10,lef=95,rit=94;
void solve(int s,int N,int K,int q) {
n=N,k=K; vector <bool> o1(B);
for(int i=0;i<n;++i) o1[i*k+k-1]=1;
append_store(hi1,o1);
for(int i=0;i<n*k;++i) o1[i]=o1[i]^1;
append_store(hi0,o1);
if(s==0) {
for(int i=1;i<n;i*=2) {
cmp(min(i,n-i));
append_and(XOR,XOR,SGN);
append_xor(a,b,XOR);
}
} else {
vector<bool> o2(B),o3(B),o4(B);
for(int i=0;i<n*k;++i) o2[i]=(i/k)%2;
for(int i=0;i<k;++i) o3[i]=o4[i+(n-1)*k]=1;
append_store(odd,o2);
for(int i=0;i<n*k;++i) o2[i]=o2[i]^1;
append_store(even,o2);
append_store(lef,o3);
append_store(rit,o4);
for(int i=0;i<n;++i) {
cmp(1);
const int a=0,b=1,mn=9,mx=10;
append_and(XOR,XOR,SGN);
append_xor(mx,a,XOR);
append_xor(mn,b,XOR);
append_and(mx,mx,(i&1)?odd:even);
append_and(mn,mn,(i&1)?odd:even);
if(i&1) {
append_and(tmp,a,lef);
append_or(mn,mn,tmp);
}
if((n-i)&1) {
append_and(tmp,a,rit);
append_or(mn,mn,tmp);
}
append_left(mx,mx,k);
append_or(a,mn,mx);
}
}
}
}
void construct_instructions(int s,int n,int k,int q) {
return luotianyi::solve(s,n,k,q);
}
Round #75 - 20250515
A. [P8490] Fish
题目大意
给定 \(n\times n\) 网格,每列可以覆盖一个前缀,有 \(m\) 个特殊点,如果 \((x_i\pm 1,y_i)\) 至少覆盖了一个,且 \((x_i,y_i)\) 未覆盖,获得 \(w_i\) 权值,求最大可能权值。
数据范围:\(n\le 10^5,m\le 3\times 10^5\)。
思路分析
首先朴素 dp 就是 \(f_{i,j}\) 表示第 \(i\) 列覆盖 \(j\) 个点的方案,但一个特殊点可能被左右的列同时计算。
注意到如果方案中产生单谷 \(h_{i-1}>h_i<h_{i+1}\),那么我们可以调整使得 \(h_i=0\),很显然这是不劣的。
那么我们把 \(h_i=0\) 的点当成分界点,然后对峰和谷分别 dp,即 \(f_{i,j}\) 表示上升段中 \(h_{i+1}>j\) 的最大权值,\(g_{i,j}\) 表示 \(h_i=j\) 的最大权值。
进一步观察,如果 \(h_i\ne 0\) 且 \(\ne n\),\((i,h_{i}+1)\) 一定是一个关键点,否则调整到下一个关键点位置不劣。
则只要考虑 \(j=0/j=n\),以及所有 \(f_{x_i,y_i-1},g_{x_i,y_{i}-1}\)。
转移时对相邻两列的状态双指针。
时间复杂度 \(\mathcal O(n+m\log m)\)。
代码呈现
#include<bits/stdc++.h>
#include "fish.h"
#define ll long long
using namespace std;
const int MAXN=1e5+5,MAXM=3e5+5;
ll f[MAXM],g[MAXM],F[MAXN],G[MAXN]; //f=up g=down
vector <int> id[MAXN];
ll max_weights(int n,int m,vector<int>x,vector<int>y,vector<int>w) {
for(int i=0;i<m;++i) id[x[i]+1].push_back(i);
for(int i=1;i<=n;++i) {
auto &b=id[i-1],&a=id[i];
int sa=a.size(),sb=b.size();
sort(a.begin(),a.end(),[&](int u,int v){ return y[u]<y[v]; });
if(i>1) {
ll mx=max(F[i-2],G[i-2]);
for(int j=sa-1,k=sb-1;~j;--j) {
int u=a[j];
for(;~k&&y[b[k]]>y[u];--k) mx=max(mx,g[b[k]]);
g[u]=mx+=w[u];
}
G[i]=max(G[i-1],mx);
}
if(i<n) {
ll mx=G[i-1];
for(int j=0,k=0;j<sa;++j) {
int u=a[j];
for(;k<sb&&y[b[k]]<y[u];++k) mx=max(mx,f[b[k]]);
f[u]=mx+=w[u];
}
F[i]=max(F[i-1],mx);
}
}
ll ans=0;
for(int i=1;i<=n;++i) ans=max({ans,F[i],G[i]});
for(int i=1;i<=m;++i) ans=max({ans,f[i],g[i]});
return ans;
}
*B. [P8491] Prison
题目大意
给定两个 \([1,n]\) 中的正整数 \(a,b\)(\(a\ne b\)),以及变量 \(x\),初始 \(x=0\)。
构造一个程序,每次输入 \(x\),然后查询 \(a\) 或 \(b\) 中的一个,再修改 \(x\),最终得到 \(a,b\) 的大小关系。
要求过程中的 \(x\in[0,20]\)。
数据范围:\(n\le 5000\)。
思路分析
首先可以想到逐位比较,\(x\) 上存储当前要比较二进制第几位,以及 \(a\) 的这一位是多少,或者还没有查询 \(a\)。
状态数 \(3\lceil \log_2n\rceil=39\),如果改成三进制做到 \(4\lceil\log_3n\rceil=32\)。
需要进一步优化,尝试优化掉未查询 \(a\) 的状态,这是可以做到的,即先查询 \(a\) 的最高位,然后查询 \(b\),比较最高位,并且直接记录 \(b\) 的次高位,再比较 \(a\) 的次高位。
此时轮流询问 \(a,b\),状态数 \(3\lceil \log_2n\rceil =24\)。
继续优化,如果比较到最低位,且有有一个数最低位 \(0/2\),那么根据 \(a\ne b\) 直接知道答案。
类似拓展,我们维护这两个数可能的范围 \([l,r]\),如果查询得到的数直接为 \(l\) 或 \(r\),则可以直接返回答案。
那么 \(f_n=f_{(n-2)/3}+3\),可以做到 \(21\) 个状态。
进一步不需要每次都三进制,直接 dp 转移,枚举每次询问的进制,\(f_k=\max f_{k-i}\times i+2\),可以算出 \(f_{20}\ge n\)。
一种可能的方法是只在最后一层用二进制,其他时候用三进制。
代码呈现
#include<bits/stdc++.h>
#include "prison.h"
using namespace std;
vector<vector<int>> a;
int o(int d,int c) { return d?(d-1)*3+c+1:0; }
void cdq(int d,int c,int l,int r,int L,int R) {
auto &b=a[o(d,c)];
for(int i=L;i<=l;++i) b[i]=-1-b[0];
for(int i=r;i<=R;++i) b[i]=-2+b[0];
++l,--r; if(l>r) return ;
if(l==r) return b[l]=o(d+1,0),cdq(d+1,0,l,r,l-1,r+1);
if(r-l+1<=4) {
int mid=(l+r)>>1;
for(int i=l;i<=r;++i) b[i]=o(d+1,i>mid);
cdq(d+1,0,l,mid,l-1,r+1);
cdq(d+1,1,mid+1,r,l-1,r+1);
return ;
}
int k=r-l+1,x=l+k/3,y=x+(k+2)/3;
for(int i=l;i<=r;++i) b[i]=o(d+1,(i>=y)+(i>=x));
cdq(d+1,0,l,x-1,l-1,r+1);
cdq(d+1,1,x,y-1,l-1,r+1);
cdq(d+1,2,y,r,l-1,r+1);
}
vector<vector<int>> devise_strategy(int n) {
a=vector<vector<int>>(21,vector<int>(n+1));
for(int i:{0,4,5,6,10,11,12,16,17,18}) a[i][0]=1;
cdq(0,0,1,n,1,n);
return a;
}
*C. [P8492] Towers
题目大意
给定 \(a_1\sim a_n\),保证两两不同,\((i,j)\) 有边当且仅当存在 \(i<k<j\) 使得 \(\max(a_i,a_j)\le a_k-d\)。
\(q\) 次询问 \(ql,qr,d\),求 \([ql,qr]\) 范围内的最大团。
数据范围:\(n,q\le 10^5\)。
思路分析
可以发现一个点集为团当且仅当相邻点之间有边。
然后考虑如何检验两个点之间有边,设 \([l_i,r_i]\) 表示 \(i\) 左右两侧第一个 \(\ge a_i+d\) 的点。
那么两个点有边当且仅当 \([l_i+1,r_i-1],[l_j+1,r_j-1]\) 无交。
因此求解原问题变成计算最大不相交线段数。
但这仍然不好做,注意到任意两个线段要么相交要么包含。
否则 \(l_i\le l_j\le r_i\le r_j\),则 \(a_i+d> a_{l_j}\ge a_j+d\) 且 \(a_i+d\le a_{r_i}<a_j+d\),因此矛盾。
删掉所有包含其他线段的线段,剩余的本质不同线段数就是答案。
那么考虑什么样的线段不包含其他线段,可以发现 \(a_i=\min a[l_i,r_i]\) 时其他的线段不可能被该线段包含,且每个线段恰在最小值处计数。
从全局询问开始,那么对于每个 \(i\),其有贡献的时刻一定是 \(d\) 的前缀。
具体来说,只要考虑所有 \(\ge a_i\) 的点构成的连续段 \([L_i,R_i]\),则 \(d\le \min(\max a[L_i,i],\max a[i,R_i])-a_i\) 时有贡献。
套上区间询问变成二维数点,主席树维护。
但对于区间询问,如果 \(L_i<ql\) 时,\(l_i\) 可以 \(<L_i\),只要 \(a_i\) 是 \([\max(l_i,ql),\min(r_i,qr)]\) 范围内的最小值即可。
那么对于这样的点,推一些性质:\(a_i=\min a[ql,i]\),因此考虑 \(a[ql,n]\) 的所有后缀最小值。
另一个条件是 \(\max a[ql,i]<a_i+d\),显然满足这个条件的后缀最小值是一段前缀。
并且对于一个满足条件的 \(a_i\),他后面的后缀最小值一定不满足 \(\max a[ql,i]<a_i+d\)。
所以只求出最后一个满足该条件的后缀最小值并判断即可,把后缀最小值单调栈建树,树上倍增即可维护。
时间复杂度 \(\mathcal O((n+q)\log V)\)。
代码呈现
#include<bits/stdc++.h>
#include "towers.h"
using namespace std;
const int MAXN=1e5+5,V=1e9;
int n,a[MAXN],lf[MAXN][20],rf[MAXN][20],stk[MAXN],st[MAXN][20];
int bit(int x) { return 1<<x; }
int qmx(int l,int r) {
int k=__lg(r-l+1);
return max(st[l][k],st[r-bit(k)+1][k]);
}
struct SegmentTree {
int ct[MAXN*32],ls[MAXN*32],rs[MAXN*32],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) ins(u,l,mid,ls[q],ls[p]),rs[p]=rs[q];
else ins(u,mid+1,r,rs[q],rs[p]),ls[p]=ls[q];
}
int qry(int ul,int ur,int l,int r,int p) {
if(ul<=l&&r<=ur) return ct[p];
int mid=(l+r)>>1,s=0;
if(ul<=mid) s+=qry(ul,ur,l,mid,ls[p]);
if(mid<ur) s+=qry(ul,ur,mid+1,r,rs[p]);
return s;
}
} T;
int wl[MAXN],wr[MAXN],w[MAXN],rt[MAXN];
void init(int N,vector<int>H) {
n=N;
for(int i=1;i<=n;++i) st[i][0]=a[i]=H[i-1];
int tp=0;
for(int i=1;i<=n;++i) {
while(tp&&a[stk[tp]]>a[i]) rf[stk[tp--]][0]=i;
lf[i][0]=stk[tp],stk[++tp]=i;
}
while(tp) rf[stk[tp--]][0]=n+1;
rf[n+1][0]=n+1;
for(int k=1;k<20;++k) for(int i=0;i<=n+1;++i) {
lf[i][k]=lf[lf[i][k-1]][k-1],rf[i][k]=rf[rf[i][k-1]][k-1];
}
for(int k=1;k<20;++k) for(int i=1;i+bit(k)-1<=n;++i) {
st[i][k]=max(st[i][k-1],st[i+bit(k-1)][k-1]);
}
for(int i=1;i<=n;++i) {
wl[i]=qmx(lf[i][0]+1,i)-a[i],wr[i]=qmx(i,rf[i][0]-1)-a[i];
w[i]=min(wl[i],wr[i]),T.ins(w[i],0,V,rt[i-1],rt[i]);
}
}
int max_towers(int l,int r,int d) {
++l,++r;
int ans=T.qry(d,V,0,V,rt[r])-T.qry(d,V,0,V,rt[l-1]),u=l,v=r;
for(int k=19;~k;--k) {
if(rf[u][k]<=r&&a[rf[u][k]]+d>qmx(l,rf[u][k])) u=rf[u][k];
if(lf[v][k]>=l&&a[lf[v][k]]+d>qmx(lf[v][k],r)) v=lf[v][k];
}
auto chk=[&](int x) {
return w[x]<d&&(wl[x]>=d||lf[x][0]<l)&&(wr[x]>=d||rf[x][0]>r);
};
ans+=chk(u)+(u!=v&&chk(v));
return ans;
}
D. [P8493] Circuit
题目大意
给定 \(n+m\) 个点的树,有 \(m\) 个叶子,每个点有 01 权值 \(c\),叶子的权值已知,第 \(u\) 个点的权值为 \([\sum_{v\in\mathrm{son}(u)} c_v\ge k_u]\)。
\(q\) 次翻转标号在一个区间中的叶子颜色,动态维护有多少种 \(k\) 使得 \(c_0=1\)。
数据范围:\(n,m\le 10^5\)。
思路分析
设 \(f_u\) 为 \(c_u=1\) 的概率,\(x=\sum_{v\in\mathrm{son}(u)} c_v\),则 \(f_u=\sum_i\mathrm{Pr}(x=i)\dfrac{i}{\deg_u}=\sum_{v}\dfrac{f_v}{\mathrm{deg}_u}\)。
因此 \(f_u\) 关于子树内每个叶子的 \(f_x\) 贡献独立,系数为不在 \(x\to u\) 路径上的点的 \(\deg\) 积。
换根 dp 预处理系数之后线段树维护。
时间复杂度 \(\mathcal O((n+m)\log m)\)。
代码呈现
#include<bits/stdc++.h>
#include "circuit.h"
#define ll long long
using namespace std;
const int MAXN=2e5+5,MOD=1e9+2022;
int n,m;
struct SegmentTree {
ll f[1<<19|5][2]; bool rv[1<<19|5];
void psu(int p) { for(int o:{0,1}) f[p][o]=(f[p<<1][o]+f[p<<1|1][o])%MOD; }
void adt(int p) { rv[p]^=1,swap(f[p][0],f[p][1]); }
void psd(int p) { if(rv[p]) adt(p<<1),adt(p<<1|1),rv[p]=0; }
void add(int u,int o,int k,int l=0,int r=m-1,int p=1) {
if(l==r) return f[p][o]=k,void();
int mid=(l+r)>>1;
u<=mid?add(u,o,k,l,mid,p<<1):add(u,o,k,mid+1,r,p<<1|1);
psu(p);
}
void upd(int ul,int ur,int l=0,int r=m-1,int p=1) {
if(ul<=l&&r<=ur) return adt(p);
int mid=(l+r)>>1; psd(p);
if(ul<=mid) upd(ul,ur,l,mid,p<<1);
if(mid<ur) upd(ul,ur,mid+1,r,p<<1|1);
psu(p);
}
} T;
vector <int> G[MAXN];
ll f[MAXN],w[MAXN],t[MAXN];
inline void dfs1(int u) {
if(G[u].empty()) return w[u]=1,void();
w[u]=G[u].size();
for(int v:G[u]) dfs1(v),w[u]=w[u]*w[v]%MOD;
}
inline void dfs2(int u) {
if(G[u].empty()) return ;
int s=G[u].size(); t[s-1]=1;
for(int i=s-1;i;--i) t[i-1]=t[i]*w[G[u][i]]%MOD;
ll z=f[u];
for(int i=0;i<s;++i) f[G[u][i]]=z*t[i]%MOD,z=z*w[G[u][i]]%MOD;
for(int v:G[u]) dfs2(v);
}
void init(int N,int M,vector<int>P,vector<int>A) {
n=N,m=M;
for(int i=1;i<n+m;++i) G[P[i]].push_back(i);
dfs1(0),f[0]=1,dfs2(0);
for(int i=0;i<m;++i) T.add(i,A[i],f[i+n]);
}
int count_ways(int L,int R) {
T.upd(L-n,R-n);
return T.f[1][1];
}
E. [P8494] Insects
题目大意
给定 \(n\) 个元素和一个集合 \(S\),初始为空,每次操作可以单点插入或删除,或询问交互库 \(S\) 中每种颜色出现次数的最大值。
每种操作至多进行 \(3n\) 次,求出所有颜色出现次数的最小值。
数据范围:\(n\le 2000\)。
思路分析
考虑二分答案,先求颜色数 \(c\),依次加入所有元素,如果加入后答案 \(>1\) 就删除该元素,最终 \(c=|S|\)
判断答案是否 \(\ge k\),那么依次加入 \(S\) 中的每个元素,如果返回答案 \(>k\) 就删除该元素。
这样扫一遍能保证每种颜色的元素至多 \(k\) 个,如果 \(|S|=ck\) 说明答案 \(\ge k\),否则答案 \(<k\)。
注意到答案 \(\ge k\) 时可以删掉 \(S\) 中元素,否则只要考虑 \(S\) 中元素,因此每次操作元素数减半,次数为 \(n+2n=3n\)。
但由于 \(ck\) 不一定恰好等于 \(\dfrac n2\),因此加上一些随机化和剪枝,然后就能通过了。
时间复杂度 \(\mathcal O(n)\)。
代码呈现
#include<bits/stdc++.h>
#include "insects.h"
using namespace std;
mt19937 rnd(time(0));
const int MAXN=2001;
int min_cardinality(int n) {
int siz=0,col=0;
auto add=[&](int x) -> void { move_inside(x), ++siz; };
auto del=[&](int x) -> void { move_outside(x),--siz; };
auto count=[&]() -> int { return press_button(); };
vector <int> Q,ins,ers;
for(int i=0;i<n;++i) {
add(i);
if(count()>1) del(i);
else ins.push_back(i),++col;
}
for(int u:ins) del(u);
ins.clear();
for(int i=0;i<n;++i) Q.push_back(i);
int l=1,r=n/col,res=r;
while(l<=r) {
int mid=(l+r)>>1;
ins.clear(),ers.clear();
shuffle(Q.begin(),Q.end(),rnd);
for(int u:Q) {
if(siz==mid*col) ers.push_back(u);
else {
add(u);
if(count()>mid) del(u),ers.push_back(u);
else ins.push_back(u);
}
}
if(siz==mid*col) res=mid,l=mid+1,Q=ers;
else {
Q=ins,r=mid-1;
for(int u:ins) del(u);
}
}
return res;
}
*F. [P8495] Islands
题目大意
给定 \(n\) 个点 \(m\) 条边的有向图,经过每条边后边会反向,求一条从 \(1\) 出发的非平凡回路经过每条边偶数次。
数据范围:\(n\le 10^5,m\le 2\times 10^5\)。
思路分析
首先一个没有出度的点肯定可以删掉,那么递归该操作后相当于拓扑排序,删掉了走不到环上的点。
然后每个点都有至少一个出度,给每个点选一条出边,构成一个基环内向树森林。
如果 \(1\) 的出度 \(>1\),那么加入另一条边,先走 \(1\) 所在的环再绕回来,然后走 \(x\) 所在的环绕回来,再重复一遍即可。
具体构造只需要建出基环树森林,每次任意找一条能走的边走过去即可。
如果 \(1\) 的出度 \(=1\),则只能走到下一个点,构造一个环后走回来,那么删掉这个点递归即可。
时间复杂度 \(\mathcal O(m\log m)\)。
代码呈现
#include<bits/stdc++.h>
#include "islands.h"
#define fi first
#define se second
using namespace std;
const int MAXN=1e5+5;
queue <int> Q;
vector <pair<int,int>> in[MAXN];
set <pair<int,int>> G[MAXN];
void topo() {
while(Q.size()) {
int u=Q.front(); Q.pop();
for(auto e:in[u]) if(G[e.fi].size()) {
G[e.fi].erase({u,e.se});
if(G[e.fi].empty()) Q.push(e.fi);
}
}
}
vector <int> ans;
bool c[MAXN*2];
int ct=0,s=0;
void dfs(int u,int fz) {
if(u==s&&!ct&&~fz) return ;
for(auto e:G[u]) if(e.se^fz) {
ans.push_back(e.se),c[e.se]^=1,ct+=2*c[e.se]-1;
G[e.fi].insert({u,e.se}),G[u].erase(e);
dfs(e.fi,e.se);
return ;
}
}
variant<bool,vector<int>> find_journey(int n,int m,vector<int>U,vector<int>V) {
for(int i=0;i<m;++i) G[U[i]].insert({V[i],i}),in[V[i]].push_back({U[i],i});
for(int i=0;i<n;++i) if(G[i].empty()) Q.push(i);
topo();
while(G[s].size()<2) {
if(G[s].empty()) return false;
int t=G[s].begin()->fi;
ans.push_back(G[s].begin()->se);
G[s].clear(),Q.push(s),s=t,topo();
}
int o=ans.size();
for(int i=0;i<n;++i) while(G[i].size()>1+(i==s)) G[i].erase(G[i].begin());
dfs(s,-1);
for(int i=o-1;~i;--i) ans.push_back(ans[i]);
return ans;
}
Round #76 - 20250520
A. [P11049] Nile
题目大意
给定 \(n\) 个元素,每个元素有 \(w_i,a_i,b_i\),每个点代价为 \(a_i\),可以匹配一些 \(w\) 相差 \(\le d\) 的元素对,这些元素的代价为 \(b_i\)(\(b_i<a_i\)),对 \(q\) 个 \(d\) 求最小代价。
数据范围:\(n,q\le 10^5\)。
思路分析
我们肯定贪心地将尽可能多的元素选成 \(b_i\)。
把所有元素按 \(w_i\) 排序,把相差 \(\le d\) 的相邻元素缩成连续段,对每个连续段分讨最小代价:
- 长度为偶数:所有元素都能取到 \(b_i\)。
- 长度为奇数:选一个元素变成 \(a_i\),如果删去该元素,左右两侧元素个数为偶数,那么可以直接删,否则要求左右两侧 \(w\) 相差 \(\le d\)。
线段树维护最后一种情况中能删除的所有元素,连续段可以并查集维护。
时间复杂度 \(\mathcal O((n+q)\log n)\)。
代码呈现
#include<bits/stdc++.h>
#include "nile.h"
#define ll long long
using namespace std;
const int MAXN=1e5+5,inf=2e9;
struct itm { int a,b,w; } p[MAXN];
struct FenwickTree {
static const int N=1<<17;
int tr[N<<1];
void init() { memset(tr,0x3f,sizeof(tr)); }
void ins(int x,int v) {
for(tr[x+=N]=v,x>>=1;x;x>>=1) tr[x]=min(tr[x<<1],tr[x<<1|1]);
}
int qry(int l,int r) {
int s=inf;
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;
}
} T;
int bit(int x) { return 1<<x; }
int n,dsu[MAXN],c[MAXN],R[MAXN],g[MAXN][2];
ll s[MAXN];
ll val(int x) { return (R[x]-x)&1?s[x]:s[x]-min(T.qry(x,R[x]),g[x][x&1]); }
int find(int x) { return x^dsu[x]?dsu[x]=find(dsu[x]):x; }
vector<ll> calculate_costs(vector<int> W,vector<int> A,vector<int> B,vector<int> E) {
n=W.size(),T.init();
for(int i=0;i<n;++i) p[i+1]={A[i],B[i],W[i]};
sort(p+1,p+n+1,[](itm x,itm y){ return x.w<y.w; });
vector <array<int,2>> lim;
for(int i=1;i<n;++i) lim.push_back({p[i+1].w-p[i].w,-i});
for(int i=2;i<n;++i) lim.push_back({p[i+1].w-p[i-1].w,i});
sort(lim.begin(),lim.end());
ll ans=0;
for(int i=1;i<=n;++i) {
dsu[i]=i,R[i]=i,ans+=p[i].a,s[i]=c[i]=p[i].a-p[i].b;
T.ins(i,c[i]),g[i][i&1]=c[i],g[i][(i&1)^1]=inf;
}
map <int,ll> Q;
Q[0]=ans;
for(auto z:lim) {
int i=z[1];
if(i<0) {
i=-i;
int x=find(i);
ans+=val(x)+val(i+1);
g[x][0]=min(g[x][0],g[i+1][0]);
g[x][1]=min(g[x][1],g[i+1][1]);
s[x]+=s[i+1],dsu[i+1]=x,R[x]=R[i+1];
if(x<i) T.ins(i,inf);
if(i+1<R[i+1]) T.ins(i+1,inf);
ans-=val(x);
} else ans+=val(find(i)),T.ins(i,c[i]),ans-=val(find(i));
Q[z[0]]=ans;
}
vector <ll> res;
for(int x:E) res.push_back((--Q.upper_bound(x))->second);
return res;
}
B. [P12543] Rotate
题目大意
给定长度 \(5\times 10^4\) 的环以及若干射线,每次可以把一个集合内的射线转动相同的角度,要求每次旋转后两两射线的夹角之和不降,构造一组方案最大化所有夹角之和。
数据范围:\(n\le 10^5\)。
思路分析
注意到如果两条射线恰好反向,则其他所有射线到这两条射线的夹角之和都是 \(\pi\)。
可以证明所有射线两两反向时最优,否则把一个未匹配的点向答案变大的方向旋转,找到第一个和其他点反向的位置,很显然答案变大。
那么一种构造就是把所有射线排序,依次把第 \(i\) 条旋转到 \(i+\dfrac n2\) 的对称位置,证明就是每次旋转的时候无论往哪边答案都不降。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
void rotate(vector<int>t,int x);
const int m=50000;
void energy(int n,vector<int>v) {
vector<array<int,2>>a(n);
for(int i=0;i<n;++i) a[i]={v[i],i};
sort(a.begin(),a.end());
for(int i=0;i<n/2;++i) rotate({a[i][1]},(a[i+n/2][0]-a[i][0]+m/2)%m);
}
*C. [P12541] Hack
题目大意
交互器有整数 \(n\),你每次可以给定集合 \(S\),得到 \(S\) 中有多少元素对 \(\bmod n\) 同余,在询问的 \(\sum |S|\le 1.1\times 10^5\) 的情况下询问出 \(n\)。
数据范围:\(n\le 10^9\)。
思路分析
考虑一个朴素想法,即构造一个集合 \(S\),满足 \(S\) 中有同余对,然后通过类似分治的手段逐步缩减 \(|S|\) 直到 \(|S|=2\),就得到 \(n\) 的一个倍数,然后逐步去掉每个质因子即可。
首先如何减小 \(S\),可以想到二分成 \(S_0,S_1\),如果有一侧内部有同余对直接递归,否则相当于一张二分图要找到一条边,只要交替二分 \(S_0,S_1\) 即可。
然后考虑构造 \(S\),这个可以根据 BSGS 构造,即加入 \(1\sim p,n+1,n+1-p,\dots,n+1-(q-1)p\),满足 \(p\times q\ge n\) 即可。
进一步我们只要表示出 \(>\dfrac n2\) 的数,因为 \(\le \dfrac n2\) 的数不断翻倍总会有一个倍数被表示。
而且可以交替二分的时候的代价应该是 \(2|S_0|+3|S_1|\),不一定要均分,因此可以枚举算出一组最优的 \((p,q)\)。
但这还过不去,注意到首次二分 \(S\) 直接取 \(S_0=[1,p],S_1=S\setminus S_0\)。
那么 \(S_0\) 内能表示出的数就是 \([1,p-1]\),\(S_1\) 能表示出的数是 \(p,2p,\dots,(q-1)p\)。
那么检验 \(S_0,S_1\) 内部是否有边,只要用类似 BSGS 的方法构造即可,只要 \(2\sqrt p+2\sqrt q\) 个数。
注意到如果 \(S_0\) 内部有边 \(x\) 且 \(p<q\) 则 \(S_1\) 中必定有边 \(px\),所以只要检验 \(S_1\),进一步优化常数。
时间复杂度 \(\mathcal O(\sqrt n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll collisions(vector<ll>x);
const int n=1e9,P=18174,Q=27512;
mt19937 rnd(time(0));
int solve(vector<ll>a,vector<ll>b) {
if(a.size()<b.size()) swap(a,b);
if(a.size()==1) return abs(a[0]-b[0]);
int mid=a.size()/2;
vector <ll> c=b;
for(int i=0;i<mid;++i) c.push_back(a[i]);
if(collisions(c)) return solve(vector<ll>(a.begin(),a.begin()+mid),b);
return solve(vector<ll>(a.begin()+mid,a.end()),b);
}
int solve(vector<ll>a) {
if(a.size()==2) return abs(a[0]-a[1]);
shuffle(a.begin(),a.end(),rnd);
auto mid=a.begin()+(a.size()*2+4)/5;
vector<ll> L(a.begin(),mid),R(mid,a.end());
if(collisions(L)) return solve(L);
if(collisions(R)) return solve(R);
return solve(L,R);
}
int solve() {
vector <ll> a,b,c;
for(int i=1;i<=P;++i) a.push_back(i);
for(int i=0;i<Q;++i) b.push_back(n+1-i*P);
int B=sqrt(Q);
for(int i=1;i<=B;++i) c.push_back(i*P);
for(int i=Q+1;i>B;i-=B) c.push_back(i*P);
if(collisions(c)) return solve(b);
return solve(a,b);
}
int hack() {
int x=solve(),d=x;
for(int i=2;i*i<=x;++i) if(x%i==0) {
while(x%i==0&&collisions({1,d/i+1})) x/=i,d/=i;
while(x%i==0) x/=i;
}
if(x>1&&collisions({1,d/x+1})) d/=x;
return d;
}
*D. [P11050] Message
题目大意
通信题,第一个程序可以发送 \(66\) 个 \(31\) bit 字符串,但是每次其中 \(15\) 个位置会被交互器打乱(只有先手知道这些位置)。
后手一次性接受所有字符串,要求传输长度 \(\le 1024\) 的 01 串。
思路分析
给字符串末尾加一个 \(1\),变成传输 \(1025\) 个 bit。
朴素想法是用 \(5\) 次发送传输每个正常位置的下标,但我们要和打乱的位置做区分。
注意到正常的 bit 个数多余非正常 bit 个数,因此每个正常位置上传递下一个正常位置的下标,最终每个点连向发送得到的点,则我们传递的是一个 \(16\) 元环。
很显然无论交互器如何干扰也一定能区分出来。
接下来要优化发送信息耗费的 bit,如果只发送到下一个位置的距离 \(d\),那么距离总和为 \(31\),直接用 \(d\) 个 \(0\) 和一个 \(1\) 表示即可。
此时恰好耗费 \(31\) bit,剩余 \(1025\) bit。
代码呈现
#include<bits/stdc++.h>
#include "message.h"
using namespace std;
typedef vector<bool> arr;
void send_message(arr M,arr C) {
M.push_back(1),M.resize(1025,0);
vector <int> q;
for(int i=0;i<31;++i) if(!C[i]) q.push_back(i);
q.push_back(q[0]+31);
vector <arr> Z(66,arr(31,0));
int k=0;
for(int i=0;i<16;++i) {
int d=q[i+1]-q[i],p=q[i];
Z[d-1][p]=1;
for(int j=d;j<66;++j) Z[j][p]=M[k++];
}
for(auto&i:Z) send_packet(i);
}
arr receive_message(vector<arr> R) {
vector <int> fa(31),v(31,0);
for(int i=0;i<31;++i) {
fa[i]=i;
for(int j=0;j<66;++j) if(R[j][i]) { fa[i]=(i+j+1)%31; break; }
}
vector <int> s;
for(int i=0;i<31;++i) if(!v[i]) {
vector <int> c;
for(int x=i;v[x]<2;x=fa[x]) if(v[x]++) c.push_back(x);
if(c.size()==16) { s=c; break; }
}
sort(s.begin(),s.end());
arr Z;
for(int i:s) for(int k=(fa[i]+31-i)%31;k<66;++k) Z.push_back(R[k][i]);
while(!Z.back()) Z.pop_back(); Z.pop_back();
return Z;
}
*E. [P11052] Hieroglyphs
题目大意
给定序列 \(a,b\),求一个公共子序列 \(C\) 使得所有 \(A,B\) 的公共子序列都是 \(C\) 的子序列,或报告不存在。
数据范围:\(n=|a|,m=|b|\le 10^5\)。
思路分析
先考虑保证有解(记为 \(c\))的情况。
如果一种字符在 \(a\) 中出现 \(x\) 次,\(b\) 中出现 \(y\) 次,那么这种字符必须在 \(c\) 中出现 \(\min(x,y)\) 次。
那么把出现次数较少的一侧的元素标记为关键位,我们要把所有关键位在另一个序列中找到匹配,且匹配两两不交。
考虑两个序列中的第一个关键位 \(a_p,b_q\),如果他们相等,直接匹配即可。
如果 \(a_p\) 在 \(b[1,q)\) 中未出现,则必须 \(b_q\) 匹配 \(a[1,p)\),反之亦然。
如果 \(a_p\in b[1,q)\) 且 \(b_q\in a[1,p)\),还要进一步分析决策。
如果 \(a(p,n]\) 中 \(b_q\) 的出现次数小于 \(b[q,m]\) 中的出现次数,那么 \(a_p\) 不能匹配 \(b[1,q)\),反之亦然。
加上这个限制后每个点的决策唯一。
如果此时两个条件同时满足:考虑 \(a_pb_q\dots b_q\),\(b_q\) 个数等于 \(b[q,m]\) 中所有 \(b_q\) 个数,这个序列是 \(a,b\) 的子序列,且为了保证 \(c\) 包含这个子序列,\(a_p\) 必须匹配 \(b[1,q)\)。
类似构造 \(b_qa_p\dots a_p\) 就导出了矛盾,因此这种情况直接会让答案无解。
那么构造一个可能解的时间复杂度 \(\mathcal O(n+m)\)。
接下来只要对 \(c\) 进行判定是否正确。
构造一个 LCS \(c'\) 使得 \(c'\) 不是 \(c\) 的子序列。
依次加入 \(c'\) 的每个字符,维护在 \(a,b,c\) 的子序列自动机上状态 \(a_p,b_q,c_r\),以及 \(c_r\) 对应 \(a,b\) 中的字符 \(a_{p'},b_{q'}\)。
显然 \(p\le p',q\le q'\),如果 \(p<p',q<q'\) 同时成立,那么我们直接加上 \(c[r,|c|]\) 的序列,一定能匹配 \(a,b\) 且匹配不上 \(c\)。
可以证明 \(c\) 不合法当且仅当出现 \(p<p',q<q'\) 的情况。
首先 \(p=p',q=q'\) 的状态最多转移到 \(p<p',q=q'\) 或 \(p=p',q<q'\) 的状态。
考虑一个 \(p=p',q<q'\) 的状态下一步的转移,设下一个字符为 \(c\),如果 \(c\) 不在 \(b(q,q']\),那么和 \(q=q'\) 是等价的。
否则转移后的 \(q\) 依然 \(<q'\),只要判断是否有 \(p<p'\) 即可,可以证明不合法状态只能从这种情况或对称状态转移而来。
我们枚举这种时候的 \(p\)(必须是非关键字符),然后算出能走到 \(p\) 时最小的 \(q\)。
如果存在 \(r\) 使得 \(p'_r<p,q\le q'_r\) 那么不合法,因为此时的字符串一定走到 \(c_{r}\) 后面的位置。
维护最小的 \(q\)(记为 \(f_p\))相当于在 \(f_{lst_p}\sim f_{p-1}\) 中找最小值,然后子序列自动机上添加字符 \(a_p\),可以单调栈维护 + 二分维护。
时间复杂度 \(\mathcal O((n+m)(\log n+\log m))\)。
代码呈现
#include<bits/stdc++.h>
#include"hieroglyphs.h"
using namespace std;
const int MAXN=1e5+5,V=2e5;
struct ds {
int n,p=1,a[MAXN],ps[V+5],nxt[V+5],ct[V+5];
void init() {
for(int i=0;i<=V;++i) ps[i]=n+1;
for(int i=n;i>=1;--i) nxt[i]=ps[a[i]],ct[i]=ct[nxt[i]]+1,ps[a[i]]=i;
}
int q(int x) { return ct[ps[x]]; }
void del(int x) { for(;p<x;++p) ps[a[p]]=nxt[p]; }
} ca,cb,va,vb;
int n,m,a[MAXN],b[MAXN],k,c[MAXN],p[MAXN],q[MAXN];
int st[MAXN],f[MAXN],ps[V+5];
vector <int> o[V+5];
bool chk() {
for(int i=0;i<=V;++i) o[i].clear(),ps[i]=0;
for(int i=1;i<=m;++i) o[b[i]].push_back(i);
int tp=0;
for(int i=1,j=0;i<=n;++i) {
int x=a[i];
f[i]=f[*lower_bound(st,st+tp+1,ps[x])];
f[i]=(o[x].empty()||o[x].back()<=f[i])?m+1:*upper_bound(o[x].begin(),o[x].end(),f[i]);
while(p[j+1]<i) ++j;
if(p[j+1]!=i&&f[i]<=q[j]) return false;
while(tp&&f[st[tp]]>=f[i]) --tp;
st[++tp]=i,ps[x]=i;
}
return true;
}
vector<int> ucs(vector<int>A,vector<int>B) {
n=ca.n=va.n=A.size(),m=cb.n=vb.n=B.size();
for(int i=1;i<=n;++i) a[i]=ca.a[i]=va.a[i]=A[i-1];
for(int i=1;i<=m;++i) b[i]=cb.a[i]=vb.a[i]=B[i-1];
ca.init(),cb.init(),va.init(),vb.init();
vector <int> pa,pb;
for(int i=1;i<=n;++i) if(ca.q(a[i])<=cb.q(a[i])) pa.push_back(i);
for(int i=1;i<=m;++i) if(cb.q(b[i])<ca.q(b[i])) pb.push_back(i);
auto it=pa.begin(),jt=pb.begin();
for(int oa=0,ob=0;it!=pa.end()||jt!=pb.end();) {
int i=(it==pa.end()?n+1:*it),j=(jt==pb.end()?m+1:*jt);
va.del(oa+1),vb.del(ob+1),ca.del(i),cb.del(j);
bool fa=i<=n&&vb.q(a[i])>cb.q(a[i])&&cb.q(b[j])<=ca.q(b[j]);
bool fb=j<=m&&va.q(b[j])>ca.q(b[j])&&ca.q(a[i])<=cb.q(a[i]);
if(fa==fb) return {-1};
if(fa) ++k,c[k]=a[i],p[k]=i,q[k]=vb.ps[a[i]],vb.del(q[k]+1),oa=i,++it;
else ++k,c[k]=b[j],p[k]=va.ps[b[j]],q[k]=j,va.del(p[k]+1),ob=j,++jt;
}
p[k+1]=n+1,q[k+1]=m+1;
if(!chk()) return {-1};
swap(n,m),swap(a,b),swap(p,q);
if(!chk()) return {-1};
return vector<int>(c+1,c+k+1);
}

浙公网安备 33010602011771号