2025 NOI 做题记录(四)


\(\text{By DaiRuichen007}\)



Round #73 - 20250508

A. [P9600] Closing

Problem Link

题目大意

给定一棵 \(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

Problem Link

题目大意

交互器有一张 \(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

Problem Link

题目大意

给定 \(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

Problem Link

题目大意

给定一棵 \(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

Problem Link

题目大意

给定 \(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

Problem Link

题目大意

你需要设计一个机器人,使得其能在一个 \(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

Problem Link

题目大意

序列 \(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

Problem Link

题目大意

给定 \(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

Problem Link

题目大意

给定网格上的 \(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

Problem Link

题目大意

给定字符集大小为 \(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

Problem Link

题目大意

你要挑战 \(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

Problem Link

题目大意

给定 \(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

Problem Link

题目大意

给定 \(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

Problem Link

题目大意

给定两个 \([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

Problem Link

题目大意

给定 \(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

Problem Link

题目大意

给定 \(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

Problem Link

题目大意

给定 \(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

Problem Link

题目大意

给定 \(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

Problem Link

题目大意

给定 \(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

Problem Link

题目大意

给定长度 \(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

Problem Link

题目大意

交互器有整数 \(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

Problem Link

题目大意

通信题,第一个程序可以发送 \(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

Problem Link

题目大意

给定序列 \(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);
}
posted @ 2025-06-09 21:41  DaiRuiChen007  阅读(71)  评论(0)    收藏  举报