2025 省选做题记录(一)


By DaiRuichen007



Round #33 - 2024.12.3

A. [CF1981E] Turtle and Intersected Segments

Problem Link

题目大意

给定 n 条线段 [li,ri],有权值 ai,如果 [li,ri][lj,rj] 交非空,则在 i,j 之间连一条 |aiaj| 权值的边,求图的 MST。

数据范围:n5×105

思路分析

注意到如果 i,j,k 之间两两有边,且 ai<aj<ak,那么 (i,k) 这条边一定不会被选入 MST,因为其不如 (i,j)+(j,k) 这两条边。

因此我们只要对于每个 x,把所有 lixri 的点提取出来,按 ai 排序之后在相邻两个点之间连边,在这张图上求 MST 即可。

扫描线维护 i 的集合,显然我们只关心集合变化的时刻,用 std::set 维护集合,在插入删除的时候对前驱后继连边,此时边只有 O(n) 条。

时间复杂度 O(nlogn)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e5+5,inf=1e9+7;
int n,a[MAXN],dsu[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
void solve() {
	cin>>n;
	vector <array<int,2>> sg;
	vector <array<int,3>> edg;
	for(int i=1,l,r;i<=n;++i) {
		cin>>l>>r>>a[i],dsu[i]=i;
		sg.push_back({l,-i}),sg.push_back({r,i});
	}
	set <array<int,2>> id;
	id.insert({0,0}),id.insert({inf,0});
	auto pre=[&](int i) {
		return (*--id.lower_bound({a[i],i}))[1];
	};
	auto suf=[&](int i) {
		return (*id.upper_bound({a[i],i}))[1];
	};
	auto link=[&](int u,int v) {
		if(u&&v) edg.push_back({abs(a[u]-a[v]),u,v});
	};
	sort(sg.begin(),sg.end());
	for(auto it:sg) {
		int i=it[1];
		if(i<0) i*=-1,link(pre(i),i),link(suf(i),i),id.insert({a[i],i});
		else id.erase({a[i],i}),link(pre(i),suf(i));
	}
	sort(edg.begin(),edg.end());
	int cnt=0; ll ans=0;
	for(auto &e:edg) {
		if(find(e[1])!=find(e[2])) dsu[find(e[1])]=find(e[2]),ans+=e[0],++cnt;
	}
	cout<<(cnt==n-1?ans:-1)<<"\n";
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



B. [CF1979F] Kostyanych's Theorem

Problem Link

题目大意

交互器有一张 n 个点的图,边数 n(n1)2(n2),每次交互可以给定 d,选出 d 的所有点中度数最小的一个 u,以及不为 u 邻居的点中度数最小的一个点,然后删除 u

求出图的一条哈密尔顿路。

数据范围:n105

思路分析

首先图的边数很多,根据鸽巢原理,度数最大点的度数 n2

如果存在一个度数为 n2 的点 u,我们询问并删去之,还可以知道 u 和哪个点没有边。

构造剩余图的哈密尔顿路 st,那么 s,t 中至少有一个点和 u 有边,插入头或尾即可。

容易发现上述过程只要在边数 n(n1)2(n2) 时即可递归,因此删去 u 后的图依然可以继续此过程。

如果不存在这样的点,那么存在一个度数为 n1 的点 u,删去这个点并递归,但此时剩余图的边数不满足要求。

很显然图上度数最小的点度数 <n2,因此删去度数最小的点 v,剩余的图就可以递归了,求出哈密尔顿路 st,由于 u 对所有点有连边,那么把 u,v 依次接在 t 后面即可。

std::deque 维护哈密尔顿路径。

时间复杂度 O(n)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int n;
bool vis[MAXN];
deque <int> wys;
void dfs(int s) {
	if(s<=2) {
		for(int i=1;i<=n;++i) if(!vis[i]) wys.push_back(i);
		return ;
	}
	cout<<"? "<<s-2<<endl;
	int u,v;
	cin>>u>>v;
	if(v) {
		vis[u]=true,dfs(s-1);
		if(wys.front()!=v) wys.push_front(u);
		else wys.push_back(u);
	} else {
		int p,q;
		cout<<"? "<<0<<endl;
		cin>>p>>q;
		vis[u]=vis[p]=true,dfs(s-2);
		wys.push_back(u),wys.push_back(p);
	}
}
void solve() {
	cin>>n,wys.clear();
	dfs(n),cout<<"! ";
	for(int i:wys) cout<<i<<" "; cout<<endl;
	for(int i=1;i<=n;++i) vis[i]=false;
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



C. [CF1981F] Turtle and Paths on a Tree

Problem Link

题目大意

给定 n 个点的二叉树,点带权,将树上的边分成若干条路径,最小化每条路径上点权 MEX 的和。

数据范围:n2.5×104.

思路分析

先刻画 MEX,很显然可以改成:钦定一个数 x 不为路径上任何一个点的点权,代价为 x

那么 MEX 就是最小的 x,求最小答案能保证正确性。

于是可以 dp,设 fu,i 表示 u 子树,钦定过 ufau 的链 MEX=i

转移的时候尝试把左右子树的链闭合起来,或者把两条链在 u 处连接,再新建一条向上。

时间复杂度 O(n2)

但是我们观察到答案中不会存在 MEX 太大的链,否则可以分割成总权值和更小的子段。

对于一条 MEX=v 的链,观察其中的元素 x 的出现,把链写成 C0,x,C1,x,,x,Ck

可以把链划分成 C0,C1,,Ck,以及每个 x 加上其左右的元素。

此时 Ci 的权值 x,包含 x 的链取值 4,那么 v(k+1)x+4k,因此 k 至少是 vx 级别的。

因此权值为 v 的链无法分割,其长度至少为 vlogv 级别,由于链长 n,故 v 大约是 nlogn

可以证明 n25000v4000

时间复杂度 O(n2logn)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=25005,B=4000,inf=1e9;
int n,f[MAXN][B+5],a[MAXN];
vector <int> G[MAXN];
void dfs(int u) {
	if(G[u].empty()) {
		for(int i=1;i<=B;++i) f[u][i]=(i==a[u]?inf:0);
		return ;
	}
	if(G[u].size()==1) {
		int mn=inf,v=G[u][0];
		dfs(v);
		for(int i=1;i<=B;++i) if(i!=a[u]) mn=min(mn,f[v][i]+i);
		for(int i=1;i<=B;++i) f[u][i]=(i==a[u]?inf:min(f[v][i],mn));
		if(u==1) cout<<mn<<"\n";
		return ;
	}
	int x=G[u][0],y=G[u][1];
	dfs(x),dfs(y);
	int mn=inf,mnx=inf,mny=inf;
	for(int i=1;i<=B;++i) if(i!=a[u]) mn=min(mn,f[x][i]+f[y][i]+i),mnx=min(mnx,f[x][i]+i),mny=min(mny,f[y][i]+i);
	for(int i=1;i<=B;++i) f[u][i]=(i==a[u]?inf:min({f[x][i]+mny,f[y][i]+mnx,mn}));
	if(u==1) cout<<min(mn,mnx+mny)<<"\n";
}
void solve() {
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i],G[i].clear(),fill(f[i]+1,f[i]+B+1,inf);
	for(int i=2,fz;i<=n;++i) cin>>fz,G[fz].push_back(i);
	dfs(1);
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



*D. [CF1975H] 378QAQ and Core

Problem Link

题目大意

给定长度为 n 的字符串 s,重排 s 以最小化 s 的最大字典序子串。

数据范围:n106

思路分析

首先一个字符串的最大字典序子串一定是后缀,且由最大字符 z 开头。

z 唯一的时候显然把 z 放在最后,否则如果开头不为 z,可以把开头放在第二个 z 的前面,答案变小。

同理末尾不为 z 时把末尾放在最后一个 z 的前面,答案变小。

因此可以调整使得开头结尾都是 z,原串就可以写成 z+S1+z+S2+z++z+Sk+z 的形式。

假设 S1Sk 已知,我们可以把所有 z+Si 看成单个字符,然后这个问题变成一个 k 个字符的新问题。

因此我们只需要确定所有 Si,排序可以递归解决

并且根据朴素贪心,如果当前的最大后缀从 z+Si 开始,那么把某个 j<iSj 里的字符放到 Si 末尾更优。

如果 nk<k,那么显然每个 |Si|1,否则把 |Si|=2 的一个字符给 |Si|=0 的串显然更优。

那么每个 Si 都是一个字符,递归一次就会给每个 Si 前面加一个 z,进行 (nk)/k 轮,然后变成 nkk 的情况。

对于这种情况,显然每个字符串的开头一定是字符集中最小的 k 个元素,此时如果 Si 不是最大值,那么 z+Si 不可能成为最大后缀,那么我们只要在最大的 Si 后面继续放字符集中最小的元素,不断递归。

容易发现每次两次递归后 n 减半,因此只会递归 O(logn) 轮。

时间复杂度 O(nlogn)

代码呈现

#include<bits/stdc++.h>
using namespace std;
string core(vector <string> S) {
	if(S.empty()) return "";
	int n=S.size(),x=1;
	string z=S[n-1];
	for(int i=n-2;i>=0&&S[i]==z;--i) ++x;
	if(x==1) return z;
	int y=n-x;
	if(y>=x) {
		int w=x-1,len=w;
		vector <string> T(w,z);
		for(int i=0;i<y;) {
			int nw=len;
			for(int p=w-len;p<w&&i<y;++p,++i) {
				T[p]+=S[i];
				if(p+1<w&&S[i]<S[i+1]) nw=w-p-1;
			}
			len=nw;
		}
		return core(T)+z;
	}
	vector <string> T;
	int k=x/(y+1);
	string kz="";
	for(int i=0;i<k;++i) kz+=z;
	for(int i=0;i<y;++i) T.push_back(kz+S[i]);
	for(int i=0;i<x%(y+1);++i) T.push_back(z);
	return core(T)+kz;
}
void solve() {
	int n; string s;
	cin>>n>>s;
	vector <string> S;
	for(auto c:s) S.push_back(string(1,c));
	sort(S.begin(),S.end());
	cout<<core(S)<<"\n";
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



*E. [CF1975I] Mind Bloom

Problem Link

题目大意

给定 n 张牌,第 i 张有数字 ai,初始手牌为 S,每次可以打出手中数字最大的牌 x,从剩余的牌中等概率抽 ax 张加入手牌,求抽出所有牌的概率。

数据范围:n120

思路分析

ai 从小到大排序,反面考虑求失败的概率。

考虑划分状态,我们发现初始手牌一定按从大到小的顺序被打出,且如果 max(S)>i 一定不可能打出 i

因此我们可以按 max(S) 减小来划分状态,在 max(S) 减小之前,[1,max(S)) 中的牌不会被打出。

设计状态 dpi,j 表示仅考虑 [1,i] 中的牌,当前 |S|=j

假设 S[1,i]ci 张牌,那么剩余的 ici 张牌是对称的,每张牌在 S 的概率都是 jciici

转移时枚举 i 是否在 S 中,如果在,那么求出 max(S) 减小后还剩 k 张牌的概率并从 dpi1 转移。

那么我们要求的就是转移系数 fi,j,k,表示当前 max(S)=i,且有 j<i 的牌,在 max(S) 减小后还剩 k 张牌的概率。

计算的时候再做一遍求答案的 dp,gu,v 表示 [1,u] 中的牌有 v 张的概率。

转移 g 的时候依然考虑 u 是否 S,同理概率为 vjuj,如果 uS,那么以系数 fu,v1,qgu1,q 转移。

从大到小 dp,由 gn 转移到 gi1,在 u=i,v=j+1 时会有自环,把 gi1,k 写成关于 fi,j,k 的一次函数即可。

处理出 f 后在原序列上从小到大转移 dp

时间复杂度 O(n5)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=125,MOD=1e9+7;
int ksm(int a,int b=MOD-2) { int s=1; for(;b;a=1ll*a*a%MOD,b>>=1) if(b&1) s=1ll*s*a%MOD; return s; }
int n,a[MAXN],c[MAXN],inv[MAXN],op[MAXN];
int f[MAXN][MAXN][MAXN],g[MAXN][MAXN],h[MAXN];
int dp[MAXN][MAXN];
void F(int u,int s) { //max=u, cnt[1,u-1] = s
	if(!a[u]) return f[u][s][s]=1,void();
	if(a[u]+s>=n) return ;
	memset(g,0,sizeof(g));
	memset(h,0,sizeof(h));
	g[n][a[u]+s]=1;
	for(int i=n;i>=u;--i) for(int j=i;j>=a[u]+s;--j) { //max<=i, cnt=j
		const int p=1ll*(j-s)*inv[i-s]%MOD,z=g[i][j],w=1ll*p*z%MOD;
		if(!z) continue;
		g[i-1][j]=(g[i-1][j]+1ll*(1+MOD-p)*z)%MOD;
		for(int k=j+a[i]-1;k<i;++k) {
			if(i==u&&j==s+1) h[k]=(h[k]+w)%MOD;
			else g[i-1][k]=(g[i-1][k]+1ll*w*f[i][j-1][k])%MOD;
		}
	}
	for(int i=s;i<u;++i) f[u][s][i]=1ll*g[u-1][i]*ksm(1+MOD-h[i])%MOD;
}
void solve() {
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]);
	for(int i=1;i<=n;++i) scanf("%1d",&op[i]),c[i]=c[i-1]+op[i];
	if(c[n]==n) return puts("1"),void();
	if(c[n]==0) return puts("0"),void();
	if(a[n]<=1) return puts("0"),void();
	if(a[1]>=1) return puts("1"),void();
	memset(f,0,sizeof(f));
	for(int i=n;i;--i) for(int j=i-1;~j;--j) F(i,j);
	memset(dp,0,sizeof(dp));
	dp[0][0]=1;
	for(int i=1;i<=n;++i) for(int j=c[i];j<=i;++j) { //max<=i, cnt=j
		for(int k=0;k<i;++k) {
			dp[i][j]=(dp[i][j]+1ll*f[i][j-1][k]*dp[i-1][k])%MOD;
		}
		if(!op[i]) {
			const int p=1ll*(j-c[i])*inv[i-c[i]]%MOD;
			dp[i][j]=(1ll*(1+MOD-p)*dp[i-1][j]+1ll*p*dp[i][j])%MOD;
		}
	}
	printf("%d\n",(1+MOD-dp[n][c[n]])%MOD);
}
signed main() {
	inv[1]=1;
	for(int i=2;i<MAXN;++i) inv[i]=1ll*inv[MOD%i]*(MOD-MOD/i)%MOD;
	int T; scanf("%d",&T);
	while(T--) solve();
	return 0;
}




Round #34 - 2024.12.4

A. [CF1982F] Sorting Problem Again

Problem Link

题目大意

给定 a1anq 次操作单点修改,或查询长度最小的 [l,r] 使得对 alar 排序后原序列有序。

数据范围:n,q5×105

思路分析

不妨假设 ai1n 的排列,那么我们要求的就是最长的前缀 a[1,l1] 和最长的后缀 a[r+1,n] 满足所有 ai=i

可以用线段树维护信息,维护区间左右端点值,最长的满足 ai+1ai=1 的前缀和后缀,信息合并是简单的。

如果 ai 不是排列,线段树维护比较困难,可以交换值域和下标,即把所有 iai 排序得到 p1pn(相等从小到大排序),然后在 pi 上统计和刚刚一样的问题。

用平衡树维护。

时间复杂度 O(n+qlogn),常数较大。

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
namespace IO {
int ow,olim=(1<<23)-100;
char buf[1<<23],*p1=buf,*p2=buf,obuf[1<<23];
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<23,stdin),p1==p2)?EOF:*p1++)
int read() {
	int 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 x*f;
}
void flush() {
	fwrite(obuf,ow,1,stdout),ow=0;
}
void write(int 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
}
mt19937 rnd(time(0));
int n,q,rt,a[MAXN];
struct Treap {
	struct Node {
		array<int,2> val;
		int pri,ls,rs,siz,lv,rv,lx,rx;
	}	tr[MAXN];
	inline void psu(int u) {
		const Node &x=tr[tr[u].ls],&y=tr[tr[u].rs];
		Node &t=tr[u];
		if(!t.ls&&!t.rs) {
			t.siz=1;
			t.lv=t.rv=u;
			t.lx=t.rx=1;
			return ;
		}
		if(!t.ls) {
			t.siz=1+y.siz;
			t.lv=u,t.rv=y.rv;
			if(y.lv==u+1) {
				t.lx=1+y.lx;
				t.rx=y.rx+(y.rx==y.siz);
			} else {
				t.lx=1;
				t.rx=y.rx;
			}
			return ;
		}
		if(!t.rs) {
			t.siz=x.siz+1;
			t.lv=x.lv,t.rv=u;
			if(x.rv==u-1) {
				t.lx=x.lx+(x.lx==x.siz);
				t.rx=1+x.rx;
			} else {
				t.lx=x.lx;
				t.rx=1;
			}
			return ;
		}
		t.siz=x.siz+1+y.siz;
		t.lv=x.lv,t.rv=y.rv;
		if(x.rv==u-1&&x.lx==x.siz) {
			if(y.lv==u+1) t.lx=x.siz+1+y.lx;
			else t.lx=x.siz+1;
		} else t.lx=x.lx;
		if(y.lv==u+1&&y.rx==y.siz) {
			if(x.rv==u-1) t.rx=y.siz+1+x.rx;
			else t.rx=y.siz+1;
		} else t.rx=y.rx;
	}
	inline void init(int u) {
		tr[u].val={a[u],u};
		tr[u].pri=rnd();
		tr[u].ls=tr[u].rs=0;
		tr[u].siz=1;
		tr[u].lv=tr[u].rv=u;
		tr[u].lx=tr[u].rx=1;
	}
	void split(int u,array<int,2>k,int &x,int &y) {
		if(!u) return x=y=0,void();
		if(k<tr[u].val) return y=u,split(tr[u].ls,k,x,tr[u].ls),psu(u);
		return x=u,split(tr[u].rs,k,tr[u].rs,y),psu(u);
	}
	int merge(int x,int y) {
		if(!x||!y) return x|y;
		if(tr[x].pri<tr[y].pri) return tr[x].rs=merge(tr[x].rs,y),psu(x),x;
		else return tr[y].ls=merge(x,tr[y].ls),psu(y),y;
	}
	void dfs(int u) {
		if(!u) return ;
		dfs(tr[u].ls),dfs(tr[u].rs),psu(u);
	}
	void build() {
		static int id[MAXN],stk[MAXN];
		for(int i=1;i<=n;++i) id[i]=i;
		sort(id+1,id+n+1,[&](int x,int y){ return tr[x].val<tr[y].val; });
		int tp=0;
		for(int o=1;o<=n;++o) {
			int i=id[o];
			while(tp&&tr[stk[tp]].pri>tr[i].pri) tr[i].ls=stk[tp--];
			if(tp) tr[stk[tp]].rs=i;
			stk[++tp]=i;
		}
		dfs(rt=stk[1]);
	}
}	T;
void ins(int i) {
	int x,y;
	T.split(rt,{a[i],i},x,y);
	rt=T.merge(x,T.merge(i,y));
}
void ers(int i) {
	int x,y,z,o;
	T.split(rt,{a[i],i},o,z);
	T.split(o,{a[i],i-1},x,y);
	rt=T.merge(x,z);
}
void qry() {
	auto o=T.tr[rt];
	if(o.lx==n) return IO::putstr("-1 -1\n");
	int l=(o.lv==1)?o.lx:0;
	int r=(o.rv==n)?o.rx:0;
	IO::write(l+1);
	IO::putc(' ');
	IO::write(n-r);
	IO::putc('\n');
}
void solve() {
	n=IO::read(),rt=0;
	for(int i=1;i<=n;++i) a[i]=IO::read(),T.init(i);
	T.build();
	q=IO::read(),qry();
	for(int p,v;q--;) {
		p=IO::read(),v=IO::read(),ers(p),a[p]=v,T.init(p),ins(p),qry();
	}
}
signed main() {
	ios::sync_with_stdio(false);
	int _=IO::read();
	while(_--) solve();
	IO::flush();
	return 0;
}



B. [CF1987F2] Interesting Problem

Problem Link

题目大意

给定 a1an,每次可以选定一个 ai=i 的元素并删除 ai,ai+1,求最多操作次数。

数据范围:n800

思路分析

假如我们只删除 ai,这是简单的,由于每个元素独立,因此可以任意交换删除元素的顺序。

dpi 表示 [1,i] 中最多删除 j 个元素,那么我们想要删除 i,就需要先在 [1,i1] 中删除 iai 个元素。

由于第 i 个元素删除不影响前面的元素,因此可以任意插入在前面的操作之间,只需要 idpiaii 即可。

回到原问题,此时如果删除 ai 可能会导致某个 j>iaj 被删除,这要求 [i+1,j1] 已经被删除了。

因此我们可以把删除的元素看成若干个匹配的括号,容易发现每个连续的子段都是合法括号序列,以其为单位进行 dp。

我们求出 fl,r 表示想把 a[l,r] 删空,至少要在 a[1,l1] 中删除多少元素,转移为:

  • 在外面加一对括号:即先删 a[l+1,r1],再删 al
  • 连接两个括号序列:即并行删除 a[l,k],a[k+1,r]

求出 fl,r 后用 dpl1 判断能否一次性删除 [l,r]

时间复杂度 O(n3)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=805;
int n,a[MAXN],f[MAXN][MAXN],dp[MAXN];
void solve() {
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]);
	memset(f,0x3f,sizeof(f));
	for(int i=0;i<=n;++i) f[i+1][i]=0;
	for(int len=2;len<=n;len+=2) for(int l=1,r=len;r<=n;++l,++r) {
		if((l-a[l])%2==0&&f[l+1][r-1]<=(l-a[l])/2) f[l][r]=(l-a[l])/2;
		for(int k=l+1;k<r;k+=2) f[l][r]=min(f[l][r],max(f[l][k],f[k+1][r]-(k-l+1)/2));
	}
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;++i) {
		dp[i]=dp[i-1];
		for(int j=i-1;j>0;j-=2) {
			if(dp[j-1]>=f[j][i]) dp[i]=max(dp[i],dp[j-1]+(i-j+1)/2);
		}
	}
	printf("%d\n",dp[n]);
}
signed main() {
	int T; scanf("%d",&T);
	while(T--) solve();
	return 0;
}



C. [CF1983G] Your Loss

Problem Link

题目大意

给定 n 个点的树,点有点权 aq 次询问 u,v,设 uv 路径为 p0pm,求 iapi

数据范围:n5×105,q105

思路分析

把询问拆成 uLCA 的递增路径和 LCAv 的递减路径。

先计算 F(u,d) 表示 ufad1(u) 的答案。

先拆位,对每个 k 求出 iapik 位为 1 的个数。

观察 i 的第 k 位如何变化,容易发现其以 2k+1 为周期,交替出现 0,1

那么我们可以设 fu,k 表示 urt 路径的答案,差分 fu,kfv,k 可以算出 uv 路径的答案,但这要求 2kdis(u,v)

那么求 F(u,d) 的时候把这条路径分成 d=2k1+2k2++2ks 若干段,其中 k1>k2>>ks

每段 ki 算权值是容易的,只要把 k1ki1 对应的位贡献翻转。

同理 G(u,d) 表示 ufad1(u) 的方案,gu,k 表示 rtu 路径的答案,做类似的过程即可。

时间复杂度 O(nlogn+qlog2n)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e5+5,K=19;
int n,q,a[MAXN],dep[MAXN],fa[MAXN][K],c[MAXN][K];
int f[MAXN][K],g[MAXN][K];
ll dis[MAXN];
vector <int> G[MAXN];
void dfs(int u,int fz) {
	dep[u]=dep[fz]+1,dis[u]=dis[fz]+a[u];
	fa[u][0]=fz;
	for(int k=0;k<K;++k) c[u][k]=c[fz][k]+(a[u]>>k&1?-1:1);
	for(int k=1;k<K;++k) fa[u][k]=fa[fa[u][k-1]][k-1];
	for(int k=0;k<K;++k) {
		int x=fa[u][k],y=fa[fa[u][k]][k];
		f[u][k]=f[y][k]+c[x][k]-c[y][k];
		g[u][k]=g[y][k]+c[u][k]-c[x][k];
	}
	for(int v:G[u]) if(v^fz) dfs(v,u);
}
int LCA(int u,int v) {
	if(dep[u]<dep[v]) swap(u,v);
	for(int k=K-1;~k;--k) if(dep[fa[u][k]]>=dep[v]) u=fa[u][k];
	if(u==v) return u;
	for(int k=K-1;~k;--k) if(fa[u][k]^fa[v][k]) u=fa[u][k],v=fa[v][k];
	return fa[u][0];
}
ll qf(int u,int d) {
	ll ans=0;
	for(int i=K-1;~i;--i) if(d>>i&1) {
		int v=fa[u][i];
		for(int j=0;j<i;++j) ans+=(1ll<<j)*(f[u][j]-f[v][j]);
		for(int j=K-1;j>i;--j) if(d>>j&1) ans+=(1ll<<j)*(c[u][j]-c[v][j]);
		u=v;
	}
	return ans;
}
ll qg(int u,int d) {
	ll ans=0;
	for(int i=0;i<K;++i) if(d>>i&1) {
		int v=fa[u][i];
		for(int j=0;j<i;++j) ans+=(1ll<<j)*(g[u][j]-g[v][j]);
		for(int j=i+1;j<K;++j) if(d>>j&1) ans+=(1ll<<j)*(c[u][j]-c[v][j]);
		u=v;
	}
	return ans;
}
void solve() {
	cin>>n;
	for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
	for(int i=1;i<=n;++i) cin>>a[i];
	G[0].push_back(1),a[0]=0,dfs(0,0);
	cin>>q;
	for(int u,v,w;q--;) {
		cin>>u>>v,w=LCA(u,v);
		ll ans=qf(u,dep[u]-dep[w]+1);
		if(w!=v) ans+=qg(v,dep[u]+dep[v]-2*dep[w]+1)-qg(w,dep[u]-dep[w]+1);
		cout<<ans+dis[u]+dis[v]-dis[w]*2+a[w]<<"\n";
	}
	for(int i=0;i<=n;++i) G[i].clear();
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



*D. [CF1984G] Magic Trick II

Problem Link

题目大意

给定 1n 排列 p,选定 k,每次操作可以把 p 的一个长度为 k 的子段移动到任意位置,求最大的 k 使得可以通过这个操作给 p 排序,构造方案。

数据范围:n103

思路分析

我们发现 k 相当大,大部分时候 k{n2,n3},特判掉 kn1 的情况。

我们考虑 k=n2 时能进行什么操作:

  • 显然能任意循环移位 a[1,n1],a[2,n]
  • 如果 n 是奇数,那么可以任意循环移位 a[1,n]

因此当 n 是奇数的时候一定可以还原:

从小到大枚举 i,每次操作把 i 放到 1i1 后面,使他们连续。

构造很简单,先循环移位整个排列,把 i 放在 a1,然后循环移位 a[2,n],把 1i1 放到 a[ni+2,n] 上即可。

如果 n 是偶数,那我们不一定能任意循环移位 a[1,n],只能移位偶数步。

但此时我们依然能将 i 放在 a1/an,如果 an=i,就循环移位 a[1,n1]1i1 放到 a[ni+1,n1] 即可。

但这样操作结束的时候可能会得到 a=[n,1,2,,n1],此时 k 为偶数,容易发现任意操作不能改变逆序对的奇偶性,所以得到这个排列后在 k=n2 时是无解的。

那么我们不得不取 k=n3,做法就是把 n 放到 an 上,然后在 a[1,n1] 里做 k=n2 的构造。

时间复杂度 O(n3)

代码呈现

#include<bits/stdc++.h> 
using namespace std;
vector <array<int,2>> wys;
const int MAXN=1005;
int n,a[MAXN],z;
bool spj() {
	for(int i=2;i<=n;++i) if(a[i]!=a[i-1]%n+1) return false;
	if(a[1]==1) z=n;
	else {
		z=n-1;
		for(int i=1;a[i]!=1;++i) wys.push_back({2,1});
	}
	return true;
}
void rot() { wys.push_back({3,1}),rotate(a+1,a+3,a+n+1); }
void rotL() { wys.push_back({2,1}),rotate(a+1,a+2,a+n); }
void rotR() { wys.push_back({3,2}),rotate(a+2,a+3,a+n+1); }
void build() {
	z=n-2;
	for(int i=1;i<=n;++i) {
		while(a[1]!=i) rot();
		if(i>1) while(a[n]!=i-1) rotR();
	}
	while(a[1]!=1) rot();
}
void solve() {
	cin>>n,wys.clear();
	for(int i=1;i<=n;++i) cin>>a[i];
	if(spj()) return ;
	if(n%2==1) return build();
	int inv=0;
	for(int i=1;i<=n;++i) for(int j=i+1;j<=n;++j) if(a[i]>a[j]) ++inv;
	if(inv%2==1) {
		while(a[n]!=n) {
			int x=find(a+1,a+n+1,n)-a;
			x=min(x,3),wys.push_back({x,x+1});
			rotate(a+x,a+x+n-3,a+x+n-2);
		}
		return --n,build();
	}
	z=n-2;
	for(int i=1;i<=n;++i) {
		while(a[1]!=i&&a[n]!=i) rot();
		if(i==1) continue;
		if(a[1]==i) while(a[n]!=i-1) rotR();
		else while(a[n-1]!=i-1) rotL();
	}
	while(a[1]!=1) rot();
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) {
		solve();
		cout<<z<<"\n"<<wys.size()<<"\n";
		for(auto o:wys) cout<<o[0]<<" "<<o[1]<<"\n";
	}
	return 0;
}



E. [CF1987H] Fumo Temple

Problem Link

题目大意

交互器有 n×m 矩阵 a,其中 ai,j{1,0,1},存在特殊点 (x,y),每次询问 (i,j) 会得到 |xi|+|yj|+|u[x,i],v[y,j]au,v|

n+225 次询问内求出 (x,y)

数据范围:n,m5000

思路分析

交互器返回的答案非常奇怪,不妨看成 |xi|+|yj| 加上 [0,|xi+1|×|yj+1|] 的随机数。

考虑随机化,动态维护可能成为答案的点集,每次随机询问一个点,然后删除不可能成为答案的点。

交互次数和复杂度未知,但可以通过。

代码呈现

#include<bits/stdc++.h>
using namespace std;
mt19937 rnd(time(0));
vector <array<int,2>> S,T;
void solve() {
	int n,m;
	cin>>n>>m,S.clear(),T.clear();
	for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) S.push_back({i,j});
	while(true) {
		auto z=S[rnd()%S.size()];
		cout<<"? "<<z[0]<<" "<<z[1]<<endl;
		int w; cin>>w;
		if(!w) return cout<<"! "<<z[0]<<" "<<z[1]<<endl,void();
		for(auto &p:S) {
			int x=abs(p[0]-z[0]),y=abs(p[1]-z[1]);
			if(x+y<=w&&w<=x+y+(x+1)*(y+1)) T.push_back(p);
		}
		S.swap(T),T.clear();
	}
}
signed main() {
	S.resize(5000*5000),T.resize(5000*5000);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}




Round #35 - 2024.12.5

A. [CF1990E2] Catch the Mole(Hard Version)

Problem Link

题目大意

给定 n 个点的树,交互器有一个点 u,每次可以询问 x,可以知道 u 是否在 x 子树中,如果不在,就令 ufau,在 160 次操作后确定 u 的位置。

数据范围:n5000

思路分析

考虑动态维护可能成为 u 的点集 S,询问 xS 会变成 fa(S)STx

对每个 x 求出询问后 S 的大小,贪心地选择最小化 S 的点。

但这个策略显然不优,在最开始的几次可能被诱导进入不优的点导致后续无解。

因此我们在最开始的若干次操作直接在 S 中随机一个点询问即可。

时间复杂度 O(nq),其中 q160

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5005;
mt19937 rnd(time(0));
int n,q,rt,siz[MAXN],slf[MAXN],w[MAXN],fa[MAXN];
vector <int> G[MAXN],que;
bool vis[MAXN],nv[MAXN];
void dfs0(int u,int fz) {
	fa[u]=fz;
	for(int v:G[u]) if(v^fz) dfs0(v,u);
	if(fz) G[u].erase(find(G[u].begin(),G[u].end(),fz));
}
void dfs1(int u) {
	que.push_back(u),siz[u]=1,slf[u]=G[u].empty();
	for(int v:G[u]) if(vis[v]) dfs1(v),siz[u]+=siz[v],slf[u]+=slf[v];
}
int ch[MAXN<<4];
void dfs2(int u) {
	for(int v:G[u]) if(vis[v]&&v!=q) dfs2(v),nv[u]=true;
}
void dfs3(int u) {
	nv[u]=true;
	for(int v:G[u]) if(vis[v]) dfs3(v);
}
void solve() {
	cin>>n,rt=1;
	for(int i=1;i<=n;++i) G[i].clear(),vis[i]=true;
	for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
	dfs0(1,0);
	for(int _=0;;++_) {
		que.clear(),dfs1(rt);
		if(que.size()==1) return cout<<"! "<<rt<<endl,void();
		int o;
		if(_<=20) q=que[rnd()%que.size()];
		else {
			int mw=n+1,tp=0;
			for(int u:que) {
				w[u]=max(siz[u],(siz[rt]-siz[u])-(slf[rt]-slf[u])+(rt!=1));
				mw=min(mw,w[u]);
			}
			for(int u:que) if(w[u]<=mw+4) {
				for(int x=0;x<(1<<(mw+4-w[u]));++x) ch[tp++]=u;
			}
			q=ch[rnd()%tp];
		}
		cout<<"? "<<q<<endl,cin>>o;
		fill(nv+1,nv+n+1,false);
		if(!o) {
			if(rt!=1) rt=fa[rt];
			dfs2(rt);
		} else {
			dfs3(rt=q);
		}
		copy(nv+1,nv+n+1,vis+1);
	}
}
signed main() {
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



*B. [CF1990F] Polygonal Segments

Problem Link

题目大意

给定 a1anq 次操作单点修改或者查询 [l,r] 内最长的子区间 [x,y] 使得存在一个边长恰好为 axay 的多边形。

数据范围:n2×105,q105,ai1012

思路分析

很显然 [x,y] 合法当且仅当 a[x,y] 不存在绝对众数,即 2max(axy)<i=xyai

观察发现如果 ax1<i=xyai,那么选取 [x1,y] 也一定合法。

单次查询可以 CDQ 分治,把 [l,mid] 的后缀和 [mid+1,r] 的前缀拼起来。

我们发现一个后缀 a[i,n] 可能成为最优解当且仅当 ai1jiaj

很显然这样的后缀只有 O(logV) 个,因为每个后缀都会使得序列总和翻倍。

那么动态版本就用线段树维护每个区间的候选前缀与候选后缀,合并的时候考虑最大值来自左边还是右边,双指针在另外一边求出尽可能长的前缀 / 后缀。

时间复杂度 O(nlogV+qlognlogV)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,q;
ll a[MAXN];
struct seg { int x; ll s; };
struct info {
	int l,r,ans;
	ll sum;
	vector <seg> pr,sf;
	info() { l=r=sum=0,ans=-1,pr.clear(),sf.clear(); }
	info(int i) { l=r=i,ans=-1,sum=a[i],pr=sf={{i,0}}; }
	inline friend info operator +(const info &L,const info &R) {
		if(!L.l||!R.r) return L.l?L:R;
		info X;
		X.l=L.l,X.r=R.r,X.sum=L.sum+R.sum;
		X.ans=max(L.ans,R.ans),X.pr=L.pr,X.sf=R.sf;
		for(auto i:R.pr) {
			if(a[i.x]>=L.sum+i.s) X.pr.push_back({i.x,L.sum+i.s});
		}
		for(auto i:L.sf) {
			if(a[i.x]>=i.s+R.sum) X.sf.push_back({i.x,i.s+R.sum});
		}
		vector <seg> vl=L.sf,vr=R.pr;
		int sl=vl.size(),sr=vr.size();
		vl.push_back({L.l-1,L.sum});
		vr.push_back({R.r+1,R.sum});
		auto chk=[&](int i,int j) {
			if(max(a[vl[i].x],a[vr[j].x])*2<vl[i+1].s+vr[j+1].s) {
				X.ans=max(X.ans,vr[j+1].x-vl[i+1].x-1);
			}
		};
		for(int i=0,j=-1;i<sl;++i) {
			for(;j+1<sr&&a[vl[i].x]>=a[vr[j+1].x];++j);
			if(j>=0) chk(i,j);
		}
		for(int i=0,j=-1;i<sr;++i) {
			for(;j+1<sl&&a[vr[i].x]>=a[vl[j+1].x];++j);
			if(j>=0) chk(j,i);
		}
		return X;
	}
};
struct zkwSegt {
	info tr[1<<19];
	int N;
	void init() {
		for(N=1;N<=n;N<<=1);
		for(int i=1;i<(N<<1);++i) tr[i]=info();
		for(int i=1;i<=n;++i) tr[i+N]=info(i);
		for(int i=N-1;i;--i) tr[i]=tr[i<<1]+tr[i<<1|1];
	}
	void upd(int x) {
		for(tr[x+N]=info(x),x=(x+N)>>1;x;x>>=1) tr[x]=tr[x<<1]+tr[x<<1|1];
	}
	int qry(int l,int r) {
		info sl=info(l),sr=info(r);
		for(l+=N,r+=N;l^r^1;l>>=1,r>>=1) {
			if(~l&1) sl=sl+tr[l^1];
			if(r&1) sr=tr[r^1]+sr;
		}
		return (sl+sr).ans;
	}
}	T;
void solve() {
	cin>>n>>q;
	for(int i=1;i<=n;++i) cin>>a[i];
	T.init();
	for(ll op,x,y;q--;) {
		cin>>op>>x>>y;
		if(op==1) cout<<T.qry(x,y)<<"\n";
		else a[x]=y,T.upd(x);
	}
}
signed main() {
	ios::sync_with_stdio(false);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



C. [CF1988F] Heartbeat

Problem Link

题目大意

设排列 px 个前缀最大值,y 个后缀最大值,z 个上升,权值为 ax×by×cz

对于 1in,求出所有 i 阶排列权值和。

数据范围:n700

思路分析

我们发现前缀最大值一定在最大值左边,后缀最大值一定在最大值右边,因此可以把排列分成两端。

考虑排列的左半部分,要统计的问题是 i 阶排列,前缀最大值个数为 j,上升个数为 k 的方案数 fi,j,k

转移的时候考虑插入最小值:

  • 插在开头:fi,j,kfi+1,j+1,k+1
  • 插在上升或末尾:(j+1)fi,j,kfi+1,j,k
  • 插在下降位置:(ij1)fi,j,kfi+1,j+1,k

然后需要合并前后两部分,此时前缀 / 后缀最大值的贡献已经解决,只要记录上升个数,可以处理出 ui,x,vi,x 表示长度为 i 的排列有 x 个上升的方案数。

那么转移就是 ansi+j+1ui,xvj,ycx+y+[i>0](i+ji)

朴素转移复杂度过高,可以分步,特判 i=0,先算出 wj,x=ycx+y+1vj,y,然后枚举 x 即可。

时间复杂度 O(n3)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=705,MOD=998244353;
int n;
ll a[MAXN],b[MAXN],c[MAXN],C[MAXN][MAXN],dp[MAXN];
ll f[MAXN][MAXN],g[MAXN][MAXN],u[MAXN][MAXN],v[MAXN][MAXN];
signed main() {
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=0;i<=n;++i) for(int j=C[i][0]=1;j<=i;++j) C[i][j]=(C[i-1][j]+C[i-1][j-1])%MOD;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<=n;++i) cin>>b[i];
	for(int i=0;i<n;++i) cin>>c[i];
	f[0][1]=1;
	for(int i=1;i<n;++i) {
		for(int j=0;j<i;++j) for(int k=1;k<=i;++k) {
			u[i][j]=(u[i][j]+f[j][k]*a[k+1])%MOD;
			v[i][j]=(v[i][j]+f[i-1-j][k]*b[k+1])%MOD;
		}
		if(i==n-1) break;
		memset(g,0,sizeof(g));
		for(int j=0;j<i;++j) for(int k=1;k<=i;++k) {
			const ll w=f[j][k];
			if(!w) continue;
			g[j+1][k+1]=(g[j+1][k+1]+w)%MOD;
			g[j][k]=(g[j][k]+w*(j+1))%MOD;
			g[j+1][k]=(g[j+1][k]+w*(i-1-j))%MOD;
		}
		memcpy(f,g,sizeof(f));
	}
	u[0][0]=a[1],v[0][0]=b[1];
	for(int j=0;j<n;++j) for(int y=0;y<=j;++y) dp[j+1]=(dp[j+1]+a[1]*v[j][y]%MOD*c[y])%MOD;
	for(int j=0;j<n;++j) for(int x=0;x<n;++x) {
		ll sum=0;
		for(int y=0;y<=j;++y) sum=(sum+v[j][y]*c[x+y+1])%MOD;
		for(int i=x;i+j<n;++i) if(i>0) {
			dp[i+j+1]=(dp[i+j+1]+u[i][x]*sum%MOD*C[i+j][j])%MOD;
		}
	}
	for(int i=1;i<=n;++i) cout<<dp[i]<<" \n"[i==n];
	return 0;
}



D. [CF1989F] Simultaneous Coloring

Problem Link

题目大意

n×m 棋盘,每次操作可以选择 k 个行或列,给行染红,列染蓝,代价 [k>1]k2

q 次询问,每次告诉你一个位置最终的颜色,求得到满足当前限制的棋盘的最小代价。

数据范围:n,m,q2×105

思路分析

设第 i 行在第 xi 次被操作,第 j 列在第 yj 次被操作,那么一个点的颜色就是限定 xiyjxiyj

把所有限制关系画成图,如果有强联通分量,说明强连通分量中的点一定相等,不同强连通分量中的点可以按拓扑序操作。

由于答案有凸性,因此合并两个强联通分量一定不优,答案就是每个强连通分量大小的平方和,我们就是要在 q 次加边后动态维护这个值。

这种动态连通性问题,考虑整体二分,对每条边 e 确定在什么时候 te 被缩进强连通分量。

那么我们加入时间 mid 的边然后 tarjan 缩点,如果这条边被缩进强联通分量,说明 temid,否则 te>mid

分别把这些点递归到 [l,mid][mid+1,r] 中,先递归 [l,mid],然后把强联通分量都用并查集缩成一个点再处理 [mid+1,r]

时间复杂度 O(qlogq)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=4e5+5;
vector <int> G[MAXN];
int dfn[MAXN],low[MAXN],dcnt,stk[MAXN],tp,bel[MAXN],scnt;
bool ins[MAXN];
void tarjan(int u) {
	dfn[u]=low[u]=++dcnt,stk[++tp]=u,ins[u]=true;
	for(int v:G[u]) {
		if(!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
		else if(ins[v]) low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]) {
		++scnt;
		while(ins[u]) bel[stk[tp]]=scnt,ins[stk[tp--]]=false;
	}
}
struct Edge { int u,v,t; };
int R,C,n,q,dsu[MAXN],siz[MAXN];
ll ans=0;
ll W(int k) { return k>1?1ll*k*k:0; }
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
void merge(int u,int v) {
	u=find(u),v=find(v);
	if(u==v) return ;
	ans-=W(siz[u])+W(siz[v]);
	siz[u]+=siz[v],dsu[v]=u,ans+=W(siz[u]);
}
void solve(int l,int r,vector<Edge>&E) {
	if(E.empty()) {
		for(int i=l;i<=min(r,q);++i) cout<<ans<<"\n";
		return ;
	}
	if(l==r) {
		for(auto e:E) merge(e.u,e.v);
		if(l<=q) cout<<ans<<"\n";
		return ;
	}
	int mid=(l+r)>>1;
	vector <Edge> LE,RE;
	for(auto &e:E) {
		e.u=find(e.u),e.v=find(e.v);
		if(e.t<=mid) G[e.u].push_back(e.v);
	}
	for(auto e:E) {
		if(e.t<=mid) {
			if(!dfn[e.u]) tarjan(e.u);
			if(!dfn[e.v]) tarjan(e.v);
			(bel[e.u]==bel[e.v]?LE:RE).push_back(e);
		} else RE.push_back(e);
	}
	dcnt=scnt=0;
	for(auto e:E) if(e.t<=mid) dfn[e.u]=dfn[e.v]=0,G[e.u].clear(),G[e.v].clear();
	solve(l,mid,LE),solve(mid+1,r,RE);
}
signed main() {
	ios::sync_with_stdio(false);
	cin>>R>>C>>q,n=R+C;
	for(int i=1;i<=n;++i) dsu[i]=i,siz[i]=1;
	vector <Edge> E;
	for(int i=1,u,v;i<=q;++i) {
		char op;
		cin>>u>>v>>op,v+=R;
		if(op=='R') swap(u,v);
		E.push_back({u,v,i});
	}
	solve(1,q+1,E);
	return 0;
}



*E. [CF1987G2] Spinning Round

Problem Link

题目大意

给定 1n 排列 a,定义 li,ri 表示 i 左侧 / 右侧第一个 >ai 的元素(没有设成 i)。

每个点向 li/ri 连边,有一些点已经确定连边,使得图连通并最大化直径。

数据范围:n4×105

思路分析

建出大根笛卡尔树,那么 li,ri 就是 i 的左右父亲。

很显然根节点一定会连自己,那么剩下的边必须构成数,那么根的左链必须连 ri,右链必须连 li,剩余的点随便连。

考虑树上线头 dp,对于每个 u,我们记录是否直径是否从 u 子树连向 lu/ru

fu,0/1/2 表示直径从 u 子树连向 lu/ru 或同时连向 lu,ru

转移的时候分讨 u 连边,以及直径从 u 的左儿子还是右儿子向外连接。

查询的时候枚举 LCA,如果直径端点分属 LCA 的不同子树,则是平凡的,否则答案肯定分属 LCA 儿子的不同子树,在 LCA 儿子处统计答案即可。

时间复杂度 O(n)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=4e5+5;
inline void chkmax(int &x,const int &y) { x=y>x?y:x; }
int n,rt,a[MAXN],ls[MAXN],rs[MAXN],f[MAXN][3],stk[MAXN],ans;
char str[MAXN];
void dfs(int u) {
	if(!u) return ;
	int lc=ls[u],rc=rs[u];
	dfs(lc),dfs(rc);
	f[u][0]=f[lc][0],f[u][1]=f[rc][1],f[u][2]=f[lc][0]+f[rc][1];
	chkmax(ans,f[lc][1]+f[rc][0]);
	if(u==rt) return ;
	if(str[u]!='R') {
		chkmax(f[u][0],max(f[lc][1],f[rc][0])+1);
		chkmax(f[u][2],max(f[lc][1]+f[rc][1],f[rc][2])+1);
		chkmax(ans,max(f[lc][2],f[lc][0]+f[rc][0])+1);
	}
	if(str[u]!='L') {
		chkmax(f[u][1],max(f[lc][1],f[rc][0])+1);
		chkmax(f[u][2],max(f[lc][2],f[lc][0]+f[rc][0])+1);
		chkmax(ans,max(f[lc][1]+f[rc][1],f[rc][2])+1);
	}
}
void solve() {
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]),ls[i]=rs[i]=0;
	scanf("%s",str+1);
	int tp=0;
	for(int i=1;i<=n;++i) {
		while(tp&&a[stk[tp]]<a[i]) ls[i]=stk[tp--];
		if(tp) rs[stk[tp]]=i;
		stk[++tp]=i;
	}
	rt=stk[1];
	for(int u=ls[rt];u;u=ls[u]) {
		if(str[u]=='L') return puts("-1"),void();
		str[u]='R';
	}
	for(int u=rs[rt];u;u=rs[u]) {
		if(str[u]=='R') return puts("-1"),void();
		str[u]='L';
	}
	ans=0,dfs(rt);
	printf("%d\n",ans);
}
signed main() {
	int T; scanf("%d",&T);
	while(T--) solve();
	return 0;
}




Round #36 - 2025.12.10

A. [CF1991G] Grid Reset

Problem Link

题目大意

给定 n×m 矩阵,q 次操作要求填入 1×k/k×1,不能重叠,如果某行或某列被填满,则该行或该列会被清空,构造方案。

数据范围:n,m100,q1000

思路分析

考虑贪心,竖块只填第一行,横块只填第一列,如果能产生消行就选择该位置,否则任选一个位置。

把棋盘分成左上角的 k×k,右上角的 k×(mk),左下角的 (nk)×k

很显然任何时候每个子区域都是完整的若干行或若干列。

满足该条件时一定有解:假设一个横块填不了,那么左上角和左下角都必须是若干列,但在右下角是若干列的时候,一定是在左上角放了竖块引发的消行,从而左上区域不存在竖块,可以放横块。

时间复杂度 O(nmq)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=105;
int n,m,k,q;
string str;
bool a[MAXN][MAXN],ti[MAXN],tj[MAXN];
void fls() {
	fill(ti+1,ti+n+1,1),fill(tj+1,tj+m+1,1);
	for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) if(!a[i][j]) ti[i]=tj[j]=0;
	for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) if(ti[i]||tj[j]) a[i][j]=0;
}
void solve() {
	cin>>n>>m>>k>>q>>str;
	memset(a,0,sizeof(a));
	for(char op:str) {
		if(op=='H') {
			int x=0;
			for(int i=1;i<=n;++i) {
				bool ok=1;
				for(int j=1;j<=k;++j) ok&=!a[i][j];
				if(ok) x=i;
				for(int j=k+1;j<=m;++j) ok&=a[i][j];
				if(ok) break;
			}
			cout<<x<<" "<<1<<"\n";
			for(int j=1;j<=k;++j) a[x][j]=1;
		} else {
			int y=0;
			for(int j=1;j<=m;++j) {
				bool ok=1;
				for(int i=1;i<=k;++i) ok&=!a[i][j];
				if(ok) y=j;
				for(int i=k+1;i<=n;++i) ok&=a[i][j];
				if(ok) break;
			}
			cout<<1<<" "<<y<<"\n";
			for(int i=1;i<=k;++i) a[i][y]=1;
		}
		fls();
	}
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



B. [CF1993E] Xor-Grid Problem

Problem Link

题目大意

给定 n×m 矩阵,每次操作选择一行或一列,把该行的所有值替换为所在列的异或和,或把该列的所有值替换为所在行的异或和,最小化所有相邻元素的差的绝对值之和。

数据范围:n,m15

思路分析

把某个元素替换为整行异或和,等价于交换整行异或和和当前元素。

那么添加一个虚拟行和虚拟列,虚拟列的元素就是所在行的异或和,虚拟行的元素就是所在列的异或和。

那么操作就是交换虚拟行和某一行,或交换虚拟列和某一列。

看成删去一行一列后任意重排行列,重排时行列贡献独立,分别状压 dp 求出最小权哈密顿路。

注意到考虑行之间的贡献只需要枚举删除了哪一列,删除每一行的贡献都能直接计算。

n,m 同阶,时间复杂度 O(n32n)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int inf=1e9;
int n,m,a[16][16],w[16][16],dp[1<<16][16],res[16][16];
inline void chkmin(int &x,const int &y) { x=y<x?y:x; }
void DP(int q) {
	memset(dp,0x3f,sizeof(dp));
	for(int i=0;i<q;++i) dp[1<<i][i]=0;
	for(int s=0;s<(1<<q);++s) {
		for(int i=0;i<q;++i) if(s>>i&1) {
			for(int j=0;j<q;++j) if(!(s>>j&1)) {
				chkmin(dp[s|(1<<j)][j],dp[s][i]+w[i][j]);
			}
		}
	}
}
void solve() {
	cin>>n>>m;
	memset(a,0,sizeof(a));
	memset(res,0,sizeof(res));
	for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) {
		cin>>a[i][j],a[0][j]^=a[i][j],a[i][0]^=a[i][j],a[0][0]^=a[i][j];
	}
	for(int i=0;i<=n;++i) {
		memset(w,0,sizeof(w));
		for(int u=0;u<=m;++u) for(int v=u+1;v<=m;++v) {
			for(int j=0;j<=n;++j) if(i!=j) w[u][v]+=abs(a[j][u]-a[j][v]);
			w[v][u]=w[u][v];
		}
		DP(m+1);
		int U=(1<<(m+1))-1;
		for(int j=0;j<=m;++j) {
			int mn=inf;
			for(int k=0;k<=m;++k) if(k!=j) chkmin(mn,dp[U-(1<<j)][k]);
			res[i][j]+=mn;
		}
	}
	for(int i=0;i<=m;++i) {
		memset(w,0,sizeof(w));
		for(int u=0;u<=n;++u) for(int v=u+1;v<=n;++v) {
			for(int j=0;j<=m;++j) if(i!=j) w[u][v]+=abs(a[u][j]-a[v][j]);
			w[v][u]=w[u][v];
		}
		DP(n+1);
		int U=(1<<(n+1))-1;
		for(int j=0;j<=n;++j) {
			int mn=inf;
			for(int k=0;k<=n;++k) if(k!=j) chkmin(mn,dp[U-(1<<j)][k]);
			res[j][i]+=mn;
		}
	}
	int ans=inf;
	for(int i=0;i<=n;++i) for(int j=0;j<=m;++j) chkmin(ans,res[i][j]);
	cout<<ans<<"\n";
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



C. [CF1993F] Dyn-scripted Robot

Problem Link

题目大意

给定 w×h 矩形网格,以及一条长度为 q 的操作序列,一个点从 (0,0) 开始按操作序列向上下左右运动,当一个点超出左右边界,就翻转操作序列中的左右操作,当一个点超出上下边界,就翻转操作序列中的上下操作。

查询执行 k 次操作序列后会经过多少次原点。

数据范围:n106

思路分析

如果遇到边界后不翻转操作,那么这个点会在无穷大网格上运动,划分成若干 w×h 的矩形区域。

如果这个点当前位置和起点左右间隔奇数个区域,那么当前经过奇数条左右边界,同理如果这个点当前位置和终点上下间隔奇数个区域,那么当前经过奇数条上下边界。

因此每个点都唯一对应原始 w×h 区域的一个点,即左右间隔奇数个区域时左右对称,上下间隔奇数个区域时上下对称。

那么对应 (0,0) 的点就是所有 (x,y) 满足 2wx,2hy 的点。

因此枚举操作序列的位置,相当于求有多少个 i<k 满足 sx+i×dx0(mod2w),sy+i×dy0(mod2h),先简化直接关于 i 的方程,然后 exgcd 求最小解和周期,即可计算 i 的个数。

时间复杂度 O(n)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll exgcd(ll a,ll b,ll &x,ll &y) {
	if(!b) return x=1,y=0,a;
	ll g=exgcd(b,a%b,y,x);
	return y-=a/b*x,g;
}
ll inv(ll a,ll p) {
	ll x,y; exgcd(a,p,x,y);
	return (x%p+p)%p;
}
const int MAXN=1e6+5;
int px[MAXN],py[MAXN];
char str[MAXN];
void solve() {
	ll _,k,n,m,ans=0;
	cin>>_>>k>>m>>n>>(str+1),n*=2,m*=2;
	for(int i=1;i<=_;++i) {
		px[i]=(px[i-1]+(str[i]=='D'?n-1:str[i]=='U'))%n;
		py[i]=(py[i-1]+(str[i]=='L'?m-1:str[i]=='R'))%m;
	}
	ll dx=px[_],dy=py[_],gx=__gcd(dx,n),gy=__gcd(dy,m);
	ll p=n/gx,q=m/gy,ix=inv(dx/=gx,p),iy=inv(dy/=gy,q);
	ll u,v,d=exgcd(p,q,u,v),e=p/d*q;
	for(int i=1;i<=_;++i) {
		ll rx=(n-px[i])%n,ry=(m-py[i])%m;
		if(rx%gx||ry%gy) continue;
		rx=rx/gx*ix%p,ry=ry/gy*iy%q;
		//k mod p = rx, k mod q = ry
		if((ry-rx)%d) continue;
		ll z=q/d,s=((ry-rx)/d*u%z+z)%z,k0=s*p+rx;
		if(k0<k) ans+=(k-k0-1)/e+1;
	}
	cout<<ans<<"\n";
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



*D. [CF1991H] Prime Split Game

Problem Link

题目大意

给定 n 个数 a1an,每次操作可以选择 x,然后删除 x 个数,再选择另外的 x 个数,把每个数写成两个质数之和,并把两个质数加入 a

两个人轮流操作,不能操作的人输,问谁必胜。

数据范围:n,ai2×105

思路分析

考虑 a=[1,x],此时两个人轮流分拆 x,观察那些 x 使得先手必胜。

如果 x 为奇数,那么只能拆成 2,x2,要求 x2 为质数,且 x2 先手必败。

如果 x 为偶数,当 x>4,只能拆成两个奇质数 p,q

如果 p 是先手必胜的,那么后手可以删除 q,拆分 p,此时先手必败。

因此 x 先手必胜当且仅当存在两个先手必败的质数 p,q 满足 p+q=x

回到原问题,从边界条件开始:如果 a1an 全部是先手必败数。

那么对于每个被拆分的数 x1xk,先手此时无论怎么操作都会产生至少一个先手必胜的数 p1pk

那么后手可以选择 p1pk 并分别拆分成两个先手必败态,然后删除 q1qk

此时后手有必胜策略。

那么先手的目标就是把所有先手必胜的数删除掉。

很显然先手可以拆分 k 个先手必胜的数,再删除 k 个其他数,如果 n 为偶数,先手可以处理掉任意多个先手必胜数,如果 n 为奇数,先手最多处理 n1 个先手必胜数。

最后仅剩的情况是 n 个数,且全部都是先手必胜数。

此时先手不一定必败,如果存在一个数可以拆成两个先手必胜数,那么先手拆分之,就可以把同样的局面留给对手。

我们称这种数为“二阶必胜数”,二阶必胜数就是所有质数的先手必胜数的和。

如果不存在二阶必胜数,那么至少会给后手留一个先手必胜数,那么先手必败。

否则先手的目标变为删除所有二阶必胜数,同上,要求二阶必胜数的个数为 [1,n1] 之间。

如果二阶必胜数有 n 个,那是否存在类似的“三阶必胜数”能拆成两个二阶必胜数之和?

很显然这是不行的,因为所有质必胜数都是奇数,所以二阶必胜数都是偶数,那么不可能拆出二阶必胜数。

所以二阶必胜数有 n 个时先手必败。

我们只要求出所有先手必胜数和二阶必胜数,这是一个卷积问题,可以 NTT 或者 std::bitset,注意到我们求的是能否把 x 分解两个质必胜 / 非必胜数的和,因此复杂度 O(Vπ(V)ω)

时间复杂度 O(Vπ(V)ω+n)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
bitset <MAXN> isp,f,g,o,t; //f: win, g: double win
void solve() {
	int n,cf=0,cg=0;
	cin>>n;
	for(int i=1,x;i<=n;++i) cin>>x,cf+=f[x],cg+=g[x];
	if(!cf) cout<<"Bob\n";
	else if(n%2==0||cf<n) cout<<"Alice\n";
	else if(cg==0||cg==n) cout<<"Bob\n";
	else cout<<"Alice\n";
}
signed main() {
	const int n=2e5;
	isp.set(),isp[0]=isp[1]=0;
	for(int i=2;i<=n;++i) if(isp[i]) for(int j=2*i;j<=n;j+=i) isp[j]=0;
	f[4]=1;
	for(int i=3;i<=n;i+=2) {
		if(isp[i-2]&!f[i-2]) f[i]=1;
		if(isp[i]&&!f[i]) o[i]=1;
	}
	for(int i=3;i<=n;i+=2) if(o[i]) t=o,t<<=i,f|=t;
	o.reset();
	for(int i=3;i<=n;i+=2) if(f[i]&&isp[i]) o[i]=1;
	for(int i=3;i<=n;i+=2) if(o[i]) t=o,t<<=i,g|=t;
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



*E. [CF1991I] Grid Game

Problem Link

题目大意

给定 n×m 网格,你需要给每个格子赋 1nm 权值(不重复),然后和交互器进行如下游戏:

  • 两人轮流选择网格中的一个格子(不可重复),交互器先手,除第一步外,所选的格子必须和某个选过的格子有公共边。

你需要合适的赋权并构造策略,使得你选出的格子上元素总和总是小于交互器选出的。

数据范围:4n,m10

思路分析

先考虑 n,m 都是奇数的情况,此时交互器会比你多选一个格子,很显然你可以让交互器拿到 nm,你只要保证在其他 nm1 个格子中不比交互器落后太多即可。

这是容易做到的,把 nm1 个格子两两匹配,使得匹配的格子相邻,且填入的元素连续,此时交互器取一个格子,你就可以立刻取其匹配的格子,此时每对匹配最多使得你比交互器权值大 1,由于 nm12<nm,因此你必胜。

对于 n,m 有一个是偶数的情况,依然用上述的匹配方式构造无法获胜,我们必须主动构造一些可以使得我们的权值小于交互器权值的结构。

一个自然的想法是在中间放一个较小值,在四周放较大值,此时先手不可能取到极小值,而后手在先手取走一个较大值后可以取走较小值。

但这样一个结构占用五个格子,因此到最后被迫取走较大值的人其实是你。

那么我们把这种结构放在网格边界,此时较小值周围只有三个格子,填较大值后,一定是交互器取较大值而你取走较小值。

注意交互器可以从第一步一个较小值开始,从而让你亏损一些贡献,因此我们要多构造几个这种结构才能确保获胜。

事实上构造四个这样的结构即可,权值分别为:(4,nm,nm1,nm2),(3,nm3,nm4,nm5),(2,nm6,nm7,nm8),(1,nm9,nm10,nm11)

可以证明任何情况下,交互器至少会比你多 2nm28,剩下的位置两两结成匹配,至多比交互器多 nm162,最终你至少比交互器少 1.5nm20>0,因此你有必胜策略。

构造时只要把一个网格划分成四个靠着边界的 T 块和若干个 1×2 即可,按 m=4/5/6 分类构造即可。

时间复杂度 O(nm)

代码呈现

n,m 都为奇数的时候采取了一些不同的构造。

#include<bits/stdc++.h>
using namespace std;
const int dx[]={0,0,1,-1},dy[]={1,-1,0,0};
struct poi { int x,y; };
vector <poi> g[105];
int n,m,a[15][15],b[15][15],rv,lv,q;
bool vis[15][15];
void I(int x,int y,char op='D') { //op=R/D
	++q,b[x][y]=q,a[x][y]=rv--;
	g[q].push_back({x,y});
	(op=='R')?++y:++x;
	b[x][y]=q,a[x][y]=rv--;
	g[q].push_back({x,y});
}
void T(int x,int y) {
	++q,b[x][y]=q,a[x][y]=lv--;
	g[q].push_back({x,y});
	for(int d:{0,1,2,3}) {
		int i=x+dx[d],j=y+dy[d];
		if(1<=i&&i<=n&&1<=j&&j<=m) {
			b[i][j]=q,a[i][j]=rv--;
			g[q].push_back({i,j});
		}
	}
}
void solve() {
	cin>>n>>m;
	if(n%2==1&&m%2==1) {
		for(int i=1;i<=n;++i) {
			for(int j=1;j<=m;++j) cout<<(i-1)*m+j<<" ";
			cout<<endl;
		}
		set <array<int,2>> Q;
		for(int o=1;o<=n*m;++o) {
			int x,y;
			if(o&1) cin>>x>>y;
			else {
				x=(*Q.begin())[0],y=(*Q.begin())[1];
				cout<<x<<" "<<y<<endl;
			}
			vis[x][y]=true,Q.erase({x,y});
			for(int d:{0,1,2,3}) {
				int i=x+dx[d],j=y+dy[d];
				if(1<=i&&i<=n&&1<=j&&j<=m&&!vis[i][j]) Q.insert({i,j});
			}
		}
		return ;
	}
	lv=4,rv=n*m;
	if(m==4) {
		T(1,2),T(2,4);
		if(n%2==0) {
			T(n-1,1),T(n,3);
			for(int i=2;i<=n-3;i+=2) I(i,1);
			for(int i=3;i<=n-2;i+=2) I(i,2),I(i,3);
			for(int i=4;i<=n-1;i+=2) I(i,4);
		} else {
			T(3,1),T(n,2);
			for(int i=5;i<=n-1;i+=2) I(i,1);
			for(int i=4;i<=n-2;i+=2) I(i,2);
			for(int i=3;i<=n-1;i+=2) I(i,3);
			for(int i=4;i<=n;i+=2) I(i,4);
		}
	} else if(m==5) {
		T(2,1),T(1,3),T(2,5);
		T(n,2),I(n-1,3,'R'),I(n,4,'R');
		for(int i=4;i<=n-1;i+=2) I(i,1),I(i,5);
		for(int i=3;i<=n-2;i+=2) I(i,2),I(i,3),I(i,4);
	} else {
		T(1,2),T(1,m-1),T(3,1),T(3,m);
		if(m%2==1) {
			I(1,4),I(2,3),I(2,5),I(3,4);
			I(4,2,'R'),I(4,5,'R');
			for(int i=5;i<=m-3;i+=2) I(1,i,'R');
			for(int i=6;i<=m-2;i+=2) I(2,i,'R'),I(3,i,'R');
			for(int i=7;i<=m-1;i+=2) I(4,i,'R');
		} else {
			for(int i=4;i<=m-3;i+=2) I(1,i,'R');
			for(int i=3;i<=m-2;i+=2) I(2,i,'R'),I(3,i,'R');
			for(int i=2;i<=m-1;i+=2) I(4,i,'R');
		}
		if(m%2==0) {
			for(int i=5;i<=n;++i) for(int j=1;j<=m;j+=2) I(i,j,'R');
		} else {
			for(int i=5;i<=n;i+=2) for(int j=1;j<=m;++j) I(i,j);
		}
	}
	for(int i=1;i<=n;++i) {
		for(int j=1;j<=m;++j) cout<<a[i][j]<<" ";
		cout<<endl;
	}
	for(int i=1;i<=q;++i) {
		sort(g[i].begin(),g[i].end(),[&](auto u,auto v){ return a[u.x][u.y]<a[v.x][v.y]; });
	}
	for(int o=1,x,y;o<=n*m;++o) {
		if(o&1) { cin>>x>>y,vis[x][y]=true; continue; }
		else {
			for(auto z:g[b[x][y]]) if(!vis[z.x][z.y]) {
				vis[z.x][z.y]=true,cout<<z.x<<" "<<z.y<<endl;
				break;
			}
		}
	}
}
signed main() {
	int T; cin>>T;
	while(T--) {
		solve();
		memset(vis,0,sizeof(vis));
		memset(a,0,sizeof(a));
		memset(b,0,sizeof(b));
		for(int i=1;i<=q;++i) g[i].clear();
		lv=rv=q=0;
	}
	return 0;
}
posted @   DaiRuiChen007  阅读(23)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示