2025 省选做题记录(一)


\(\text{By DaiRuichen007}\)



Round #33 - 2024.12.3

A. [CF1981E] Turtle and Intersected Segments

Problem Link

题目大意

给定 \(n\) 条线段 \([l_i,r_i]\),有权值 \(a_i\),如果 \([l_i,r_i]\)\([l_j,r_j]\) 交非空,则在 \(i,j\) 之间连一条 \(|a_i-a_j|\) 权值的边,求图的 MST。

数据范围:\(n\le 5\times 10^5\)

思路分析

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

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

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

时间复杂度 \(\mathcal O(n\log n)\)

代码呈现

#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\) 个点的图,边数 \(\dfrac{n(n-1)}2-(n-2)\),每次交互可以给定 \(d\),选出 \(\ge d\) 的所有点中度数最小的一个 \(u\),以及不为 \(u\) 邻居的点中度数最小的一个点,然后删除 \(u\)

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

数据范围:\(n\le 10^5\)

思路分析

首先图的边数很多,根据鸽巢原理,度数最大点的度数 \(\ge n-2\)

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

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

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

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

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

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

时间复杂度 \(\mathcal 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\) 个点的二叉树,点带权,将树上的边分成若干条路径,最小化每条路径上点权 \(\mathrm{MEX}\) 的和。

数据范围:\(n\le 2.5\times 10^4\).

思路分析

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

那么 \(\mathrm{MEX}\) 就是最小的 \(x\),求最小答案能保证正确性。

于是可以 dp,设 \(f_{u,i}\) 表示 \(u\) 子树,钦定过 \(u\to fa_u\) 的链 \(\mathrm{MEX}=i\)

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

时间复杂度 \(\mathcal O(n^2)\)

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

对于一条 \(\mathrm{MEX}=v\) 的链,观察其中的元素 \(x\) 的出现,把链写成 \(C_0,x,C_1,x,\dots,x,C_k\)

可以把链划分成 \(C_0,C_1,\dots ,C_k\),以及每个 \(x\) 加上其左右的元素。

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

因此权值为 \(v\) 的链无法分割,其长度至少为 \(v\log v\) 级别,由于链长 \(\le n\),故 \(v\) 大约是 \(\dfrac n{\log n}\)

可以证明 \(n\le 25000\)\(v\le 4000\)

时间复杂度 \(\mathcal O\left(\dfrac{n^2}{\log n}\right)\)

代码呈现

#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\) 的最大字典序子串。

数据范围:\(n\le 10^6\)

思路分析

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

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

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

因此可以调整使得开头结尾都是 \(z\),原串就可以写成 \(z+S_1+z+S_2+z+\dots+z+S_k+z\) 的形式。

假设 \(S_1\sim S_k\) 已知,我们可以把所有 \(z+S_i\) 看成单个字符,然后这个问题变成一个 \(k\) 个字符的新问题。

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

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

如果 \(n-k<k\),那么显然每个 \(|S_i|\le 1\),否则把 \(|S_i|=2\) 的一个字符给 \(|S_i|=0\) 的串显然更优。

那么每个 \(S_i\) 都是一个字符,递归一次就会给每个 \(S_i\) 前面加一个 \(z\),进行 \(\lfloor (n-k)/k\rfloor\) 轮,然后变成 \(n-k\ge k\) 的情况。

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

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

时间复杂度 \(\mathcal O(n\log n)\)

代码呈现

#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\) 张有数字 \(a_i\),初始手牌为 \(S\),每次可以打出手中数字最大的牌 \(x\),从剩余的牌中等概率抽 \(a_x\) 张加入手牌,求抽出所有牌的概率。

数据范围:\(n\le 120\)

思路分析

\(a_i\) 从小到大排序,反面考虑求失败的概率。

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

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

设计状态 \(dp_{i,j}\) 表示仅考虑 \([1,i]\) 中的牌,当前 \(|S|=j\)

假设 \(S\)\([1,i]\)\(c_i\) 张牌,那么剩余的 \(i-c_i\) 张牌是对称的,每张牌在 \(S\) 的概率都是 \(\dfrac {j-c_i}{i-c_i}\)

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

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

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

转移 \(g\) 的时候依然考虑 \(u\) 是否 \(\in S\),同理概率为 \(\dfrac{v-j}{u-j}\),如果 \(u\in S\),那么以系数 \(f_{u,v-1,q}\)\(g_{u-1,q}\) 转移。

从大到小 dp,由 \(g_n\) 转移到 \(g_{i-1}\),在 \(u=i,v=j+1\) 时会有自环,把 \(g_{i-1,k}\) 写成关于 \(f_{i,j,k}\) 的一次函数即可。

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

时间复杂度 \(\mathcal O(n^5)\)

代码呈现

#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

题目大意

给定 \(a_1\sim a_n\)\(q\) 次操作单点修改,或查询长度最小的 \([l,r]\) 使得对 \(a_l\sim a_r\) 排序后原序列有序。

数据范围:\(n,q\le 5\times 10^5\)

思路分析

不妨假设 \(a_i\)\(1\sim n\) 的排列,那么我们要求的就是最长的前缀 \(a[1,l-1]\) 和最长的后缀 \(a[r+1,n]\) 满足所有 \(a_i=i\)

可以用线段树维护信息,维护区间左右端点值,最长的满足 \(a_{i+1}-a_i=1\) 的前缀和后缀,信息合并是简单的。

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

用平衡树维护。

时间复杂度 \(\mathcal O(n+q\log n)\),常数较大。

代码呈现

#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

题目大意

给定 \(a_1\sim a_n\),每次可以选定一个 \(a_i=i\) 的元素并删除 \(a_i,a_{i+1}\),求最多操作次数。

数据范围:\(n\le 800\)

思路分析

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

\(dp_{i}\) 表示 \([1,i]\) 中最多删除 \(j\) 个元素,那么我们想要删除 \(i\),就需要先在 \([1,i-1]\) 中删除 \(i-a_i\) 个元素。

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

回到原问题,此时如果删除 \(a_i\) 可能会导致某个 \(j>i\)\(a_j\) 被删除,这要求 \([i+1,j-1]\) 已经被删除了。

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

我们求出 \(f_{l,r}\) 表示想把 \(a[l,r]\) 删空,至少要在 \(a[1,l-1]\) 中删除多少元素,转移为:

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

求出 \(f_{l,r}\) 后用 \(dp_{l-1}\) 判断能否一次性删除 \([l,r]\)

时间复杂度 \(\mathcal O(n^3)\)

代码呈现

#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\) 个点的树,点有点权 \(a\)\(q\) 次询问 \(u,v\),设 \(u\to v\) 路径为 \(p_0\sim p_m\),求 \(\sum i\oplus a_{p_i}\)

数据范围:\(n\le 5\times 10^5,q\le 10^5\)

思路分析

把询问拆成 \(u\to \mathrm{LCA}\) 的递增路径和 \(\mathrm{LCA}\to v\) 的递减路径。

先计算 \(F(u,d)\) 表示 \(u\to fa^{d-1}(u)\) 的答案。

先拆位,对每个 \(k\) 求出 \(\sum i\oplus a_{p_i}\)\(k\) 位为 \(1\) 的个数。

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

那么我们可以设 \(f_{u,k}\) 表示 \(u\to rt\) 路径的答案,差分 \(f_{u,k}-f_{v,k}\) 可以算出 \(u\to v\) 路径的答案,但这要求 \(2^k\mid \mathrm{dis}(u,v)\)

那么求 \(F(u,d)\) 的时候把这条路径分成 \(d=2^{k_1}+2^{k_2}+\dots +2^{k_s}\) 若干段,其中 \(k_1>k_2>\dots >k_s\)

每段 \(k_i\) 算权值是容易的,只要把 \(k_1\sim k_{i-1}\) 对应的位贡献翻转。

同理 \(G(u,d)\) 表示 \(u\gets fa^{d-1}(u)\) 的方案,\(g_{u,k}\) 表示 \(rt\to u\) 路径的答案,做类似的过程即可。

时间复杂度 \(\mathcal O(n\log n+q\log^2n)\)

代码呈现

#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

题目大意

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

数据范围:\(n\le 10^3\)

思路分析

我们发现 \(k\) 相当大,大部分时候 \(k\in \{n-2,n-3\}\),特判掉 \(k\ge n-1\) 的情况。

我们考虑 \(k=n-2\) 时能进行什么操作:

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

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

从小到大枚举 \(i\),每次操作把 \(i\) 放到 \(1\sim i-1\) 后面,使他们连续。

构造很简单,先循环移位整个排列,把 \(i\) 放在 \(a_1\),然后循环移位 \(a[2,n]\),把 \(1\sim i-1\) 放到 \(a[n-i+2,n]\) 上即可。

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

但此时我们依然能将 \(i\) 放在 \(a_1/a_n\),如果 \(a_n=i\),就循环移位 \(a[1,n-1]\)\(1\sim i-1\) 放到 \(a[n-i+1,n-1]\) 即可。

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

那么我们不得不取 \(k=n-3\),做法就是把 \(n\) 放到 \(a_n\) 上,然后在 \(a[1,n-1]\) 里做 \(k=n-2\) 的构造。

时间复杂度 \(\mathcal O(n^3)\)

代码呈现

#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\times m\) 矩阵 \(a\),其中 \(a_{i,j}\in \{-1,0,1\}\),存在特殊点 \((x,y)\),每次询问 \((i,j)\) 会得到 \(|x-i|+|y-j|+|\sum_{u\in[x,i],v\in[y,j]}a_{u,v}|\)

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

数据范围:\(n,m\le 5000\)

思路分析

交互器返回的答案非常奇怪,不妨看成 \(|x-i|+|y-j|\) 加上 \([0,|x-i+1|\times |y-j+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\) 子树中,如果不在,就令 \(u\gets fa_u\),在 \(160\) 次操作后确定 \(u\) 的位置。

数据范围:\(n\le 5000\)

思路分析

考虑动态维护可能成为 \(u\) 的点集 \(S\),询问 \(x\)\(S'\) 会变成 \(fa(S)\)\(S\cap T_x\)

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

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

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

时间复杂度 \(\mathcal O(nq)\),其中 \(q\le 160\)

代码呈现

#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

题目大意

给定 \(a_1\sim a_n\)\(q\) 次操作单点修改或者查询 \([l,r]\) 内最长的子区间 \([x,y]\) 使得存在一个边长恰好为 \(a_x\sim a_y\) 的多边形。

数据范围:\(n\le 2\times 10^5,q\le 10^5,a_i\le 10^{12}\)

思路分析

很显然 \([x,y]\) 合法当且仅当 \(a[x,y]\) 不存在绝对众数,即 \(2\max(a_{x\sim y})<\sum_{i=x}^y a_i\)

观察发现如果 \(a_{x-1}< \sum_{i=x}^y a_i\),那么选取 \([x-1,y]\) 也一定合法。

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

我们发现一个后缀 \(a[i,n]\) 可能成为最优解当且仅当 \(a_{i-1}\ge \sum_{j\ge i}a_j\)

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

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

时间复杂度 \(\mathcal O(n\log V+q\log n\log V)\)

代码呈现

#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

题目大意

设排列 \(p\)\(x\) 个前缀最大值,\(y\) 个后缀最大值,\(z\) 个上升,权值为 \(a_x\times b_y\times c_z\)

对于 \(1\le i\le n\),求出所有 \(i\) 阶排列权值和。

数据范围:\(n\le 700\)

思路分析

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

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

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

  • 插在开头:\(f_{i,j,k}\to f_{i+1,j+1,k+1}\)
  • 插在上升或末尾:\((j+1)f_{i,j,k}\to f_{i+1,j,k}\)
  • 插在下降位置:\((i-j-1)f_{i,j,k}\to f_{i+1,j+1,k}\)

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

那么转移就是 \(ans_{i+j+1}\gets u_{i,x}v_{j,y}c_{x+y+[i>0]}\binom{i+j}{i}\)

朴素转移复杂度过高,可以分步,特判 \(i=0\),先算出 \(w_{j,x}=\sum_yc_{x+y+1}v_{j,y}\),然后枚举 \(x\) 即可。

时间复杂度 \(\mathcal O(n^3)\)

代码呈现

#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\times m\) 棋盘,每次操作可以选择 \(k\) 个行或列,给行染红,列染蓝,代价 \([k>1]k^2\)

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

数据范围:\(n,m,q\le 2\times 10^5\)

思路分析

设第 \(i\) 行在第 \(x_i\) 次被操作,第 \(j\) 列在第 \(y_j\) 次被操作,那么一个点的颜色就是限定 \(x_i\ge y_j\)\(x_i\le y_j\)

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

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

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

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

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

时间复杂度 \(\mathcal O(q\log q)\)

代码呈现

#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

题目大意

给定 \(1\sim n\) 排列 \(a\),定义 \(l_i,r_i\) 表示 \(i\) 左侧 / 右侧第一个 \(>a_i\) 的元素(没有设成 \(i\))。

每个点向 \(l_i/r_i\) 连边,有一些点已经确定连边,使得图连通并最大化直径。

数据范围:\(n\le 4\times 10^5\)

思路分析

建出大根笛卡尔树,那么 \(l_i,r_i\) 就是 \(i\) 的左右父亲。

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

考虑树上线头 dp,对于每个 \(u\),我们记录是否直径是否从 \(u\) 子树连向 \(l_u/r_u\)

\(f_{u,0/1/2}\) 表示直径从 \(u\) 子树连向 \(l_u/r_u\) 或同时连向 \(l_u,r_u\)

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

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

时间复杂度 \(\mathcal 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\times m\) 矩阵,\(q\) 次操作要求填入 \(1\times k/k\times 1\),不能重叠,如果某行或某列被填满,则该行或该列会被清空,构造方案。

数据范围:\(n,m\le 100,q\le 1000\)

思路分析

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

把棋盘分成左上角的 \(k\times k\),右上角的 \(k\times (m-k)\),左下角的 \((n-k)\times k\)

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

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

时间复杂度 \(\mathcal 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\times m\) 矩阵,每次操作选择一行或一列,把该行的所有值替换为所在列的异或和,或把该列的所有值替换为所在行的异或和,最小化所有相邻元素的差的绝对值之和。

数据范围:\(n,m\le 15\)

思路分析

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

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

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

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

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

\(n,m\) 同阶,时间复杂度 \(\mathcal O(n^32^n)\)

代码呈现

#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\times h\) 矩形网格,以及一条长度为 \(q\) 的操作序列,一个点从 \((0,0)\) 开始按操作序列向上下左右运动,当一个点超出左右边界,就翻转操作序列中的左右操作,当一个点超出上下边界,就翻转操作序列中的上下操作。

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

数据范围:\(n\le 10^6\)

思路分析

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

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

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

那么对应 \((0,0)\) 的点就是所有 \((x,y)\) 满足 \(2w \mid x,2h \mid y\) 的点。

因此枚举操作序列的位置,相当于求有多少个 \(i<k\) 满足 \(sx+i\times dx\equiv 0\pmod{2w},sy+i\times dy\equiv 0\pmod{2h}\),先简化直接关于 \(i\) 的方程,然后 exgcd 求最小解和周期,即可计算 \(i\) 的个数。

时间复杂度 \(\mathcal 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\) 个数 \(a_1\sim a_n\),每次操作可以选择 \(x\),然后删除 \(x\) 个数,再选择另外的 \(x\) 个数,把每个数写成两个质数之和,并把两个质数加入 \(a\)

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

数据范围:\(n,a_i\le 2\times 10^5\)

思路分析

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

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

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

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

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

回到原问题,从边界条件开始:如果 \(a_1\sim a_n\) 全部是先手必败数。

那么对于每个被拆分的数 \(x_1\sim x_k\),先手此时无论怎么操作都会产生至少一个先手必胜的数 \(p_1\sim p_k\)

那么后手可以选择 \(p_1\sim p_k\) 并分别拆分成两个先手必败态,然后删除 \(q_1\sim q_k\)

此时后手有必胜策略。

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

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

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

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

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

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

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

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

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

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

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

时间复杂度 \(\mathcal O\left(\dfrac{V\pi(V)}{\omega}+n\right)\)

代码呈现

#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\times m\) 网格,你需要给每个格子赋 \(1\sim nm\) 权值(不重复),然后和交互器进行如下游戏:

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

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

数据范围:\(4\le n,m\le 10\)

思路分析

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

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

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

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

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

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

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

事实上构造四个这样的结构即可,权值分别为:\((4,nm,nm-1,nm-2),(3,nm-3,nm-4,nm-5),(2,nm-6,nm-7,nm-8),(1,nm-9,nm-10,nm-11)\)

可以证明任何情况下,交互器至少会比你多 \(2nm-28\),剩下的位置两两结成匹配,至多比交互器多 \(\dfrac{nm-16}2\),最终你至少比交互器少 \(1.5nm-20>0\),因此你有必胜策略。

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

时间复杂度 \(\mathcal 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 @ 2025-02-07 14:45  DaiRuiChen007  阅读(83)  评论(0)    收藏  举报