[题解]【MX-S8】梦熊 CSP-S 2025 模拟赛

原比赛页面 ~ 官方题解(直播回放)

T1. P14308 【MX-S8-T1】斐波那契螺旋

发现方形的边长是斐波那契数列,增长非常快。打表发现,只要 \(91\) 个方形就能覆盖 \([-10^{18},10^{18}]\) 内的所有点。

所以模拟即可。时间复杂度 \(O(n\log V)\),底数大约是 \(\Phi=\dfrac{1+\sqrt 5}{2}\approx 1.618\)

点击查看代码
#include<bits/stdc++.h>
#define koishi x=t[0],y=t[1],xx=t[2],yy=t[3]
#define int long long
using namespace std;
const int N=1e3+5;
int t,a,b,x,y,xx,yy,f[N],idx;
signed tmp[N];
inline bool in(){return a>=x&&a<=xx&&b>=y&&b<=yy;}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>t;
	f[0]=f[1]=1;
	for(int i=2;i^N;i++) f[i]=f[i-1]+f[i-2];
	while(t--){
		cin>>a>>b;
		x=-1,y=0,xx=0,yy=1;
		for(int i=1;;i++){
			if(in()){
				cout<<f[i-1]<<"\n";
				break;
			}
			if(i%4==1){
				int t[4]{x,y-f[i],x+f[i],y};koishi;
			}else if(i%4==2){
				int t[4]{xx,y,xx+f[i],y+f[i]};koishi;
			}else if(i%4==3){
				int t[4]{xx-f[i],yy,xx,yy+f[i]};koishi;
			}else{
				int t[4]{x-f[i],yy-f[i],x,yy};koishi;
			}
		}
	}
	return 0;
}

T2. P14309 【MX-S8-T2】配对

Ref: 【MX-S8-T2】配对 - jianami

Test Case #1~5

\(O(n^2)\) 枚举点权交换。若权值为 \(1\) 的点数为奇数,再 \(O(n)\) 枚举哪个点不参与配对(相当于将该点的 \(c\) 置为 \(0\))。在交换后的图上可以 \(O(n)\) DP。

具体来说,一个子树最多只能保留一个点向上延伸是最优的。其个数取决于子树 \(u\)\(1\) 个数(记为 \(siz_u\))的奇偶性。

递归到 \(u\) 时,对于子树 \(u\) 中所有等待向上延伸的节点,先让它们跨过 \(u\) 配对。配对顺序随意,因为每个节点 \(i\) 固定产生 \(\text{dis}(i,u)\) 的贡献。

时间复杂度 \(O(n^4)\)

点击查看代码
inline bool dfs(int u,int fa){
	bool rt=a[u];//siz[u]的奇偶性
	if(ban==u) rt=0;//枚举的ban不参与配对
	for(Ed i:G[u]){
		int v=i.to,w=i.w;
		if(v==fa) continue;
		if(dfs(v,u)) cur+=w,rt^=1;
	}
	return rt;
}

Test Case #6~10

由于权值为 \(1\) 的点数为偶数,所以不需要枚举哪个点不参与配对了。

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

Solution

观察之前转移,我们的答案实际上是所有 \(siz\) 为奇数的点,到父节点的边权之和。

我们考虑如何最小化这个。

先看看我们能做什么:

  • 对于交换操作:
    • 若选择不交换,或者交换两个相同的 \(c\),则没有 \(c\) 发生变化。
    • 若选择交换两个不同的 \(c_u,c_v\),则相当于将 \(c_u,c_v\) 各自取反。
  • 对于置 \(0\) 操作:
    • 若不进行,则没有 \(c\) 发生变化。
    • 若进行,则相当于取反一个初值为 \(1\)\(c\) 值。

综上,我们所有能进行的操作就是对 \(c\) 值取反。

所以我们重新定义 \(f_{u,x,y}\) 为子树 \(u\) 中,取反了 \(x\)\(0\)\(y\)\(1\) 的最小代价。其中 \(x\in\{0,1\},y\in\{0,1,2\}\)

然后进行 \(O(n)\) 的 DP 即可,转移比较容易,可以理解为二维泛化物品的合并[1]

点击查看代码
#include<bits/stdc++.h>
#define eb emplace_back
#define int long long
using namespace std;
const int N=1e6+5;
struct Ed{int to,w;}e[N<<1];
int n,f[N][2][3],g[2][3];//取反x个0,y个1 
bitset<N> c;
vector<Ed> G[N];
inline void dfs(int u,int fa){
	f[u][0][0]=0;
	if(c[u]) f[u][0][1]=0;
	else f[u][1][0]=0;
	int x,y;
	for(auto i:G[u]) if(i.to^fa){
		dfs(i.to,u);
		memset(g,0x3f,sizeof g);
		c[u]=c[u]^c[i.to];
		for(int p1=0;p1<2;p1++){
			for(int q1=0;q1<3;q1++){
				for(int p2=0;(x=p1+p2)<2;p2++){
					for(int q2=0;(y=q1+q2)<3;q2++){
						g[x][y]=min(g[x][y],
							f[u][p1][q1]+
							f[i.to][p2][q2]+
							((c[i.to]^p2^q2)&1)*i.w);//c[i.to]+p2-q2的奇偶性 
					}
				}
			}
		}
		memcpy(f[u],g,sizeof g);
	}
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1,x;i<=n;i++) cin>>x,c[i]=x;
	for(int i=1,u,v,w;i<n;i++){
		cin>>u>>v>>w;
		G[u].eb(Ed{v,w}),G[v].eb(Ed{u,w});
	}
	memset(f,0x3f,sizeof f);
	dfs(1,0);
	if(c[1]) cout<<min(f[1][0][1],f[1][1][2]);
	else cout<<min(f[1][0][0],f[1][1][1]);
	return 0;
}

T3. P14310 【MX-S8-T3】图排列

Ref: 题解:P14310 【MX-S8-T3】图排列 - qczrz6v4nhp6u

Test Case #1

最难打的 \(5\) 分,但还是打了。

我们发现答案只和边集有关,所以 \(O(2^m)\) 枚举路径的边集。

然后判合法性,即:\(x,y\) 可以通过这些边相连,且每条边都能走到。

接下来枚举复合。

一次复合相当于将 \(p_i\gets p_{q_i}\),放在图上就是建一条 \(i\to q_i\) 的边,得到的是一个内向基环树。

复合周期就是其上所有环大小的 \(\text{lcm}\),最大为 \(6\)

所以我们可以 \(O(6^m)\) 枚举每个边参与复合的次数,然后对于每一种复合方案,扔进哈希表去重。

时间复杂度 \(O(q\times 2^m\times (n+6^m))\)

赛事枚举复合只枚举到 \(5\) 也过了,不知道是数据水还是什么(^^;

Test Case #2

最好打的 \(5\) 分。发现怎么复合集合都不会变,所以答案只可能是 No\(1\)。判一下连通性就可以了。

Test Case #8

最有启发性的 \(5\) 分。

由于后三位固定为 \(3,4,5\),相当于排列大小为 \(2\)

边权只有 \([1,2]\)\([2,1]\) 两种。

不难发现生成的集合只可能是:

  • \(\{[1,2]\}\)
    (此时只能走 \([1,2]\) 的边)
  • \(\{[1,2],[2,1]\}\)
    (此时存在 \([2,1]\) 的边被走过)

答案只能是 No\(1\)\(2\)No 先连通性判掉。

由于我们要最小化个数,所以额外建一个图,只保留权值为 \([1,2]\) 的边。

\(x,y\) 在该图上连通,则答案为 \(1\);否则为 \(2\)

不需要真的建出来,只需要并查集判一下连通性就可以了。

Test Case #9~10

后三位固定为 \(4,5\),相当于排列大小为 \(3\)

边权有 \([1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]\) 六种。

受 #8 的启发,我们仍然讨论会生成哪些集合:

  • \(\{[1,2,3]\}\)
  • \(\{[1,2,3],[1,3,2]\}\):交换 \(2,3\)
  • \(\{[1,2,3],[3,2,1]\}\):交换 \(1,3\)
  • \(\{[1,2,3],[2,1,3]\}\):交换 \(1,2\)
  • \(\{[1,2,3],[2,3,1],[3,1,2]\}\):轮换。
  • \(\{[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]\}\):全集。
如果我们生成了某一个集合,那么所有边都只能在其中选择。所以对于一个集合,将所有边权在其中的边都加入一个图中,判断 $x,y$ 在该图上的连通性。找使得 $x,y$ 连通的最小集合,输出其大小即可。

我们发现集合的数量不是 \(2^6\),而只有 \(6\)。所以猜测排列大小为 \(4,5\) 时,生成的集合个数不会很大。

Solution

我们搜索可以得出,大小为 \(4,5\) 的排列所能生成的集合个数分别是 \(30,156\) 个。

我们可以搜索 / 打表将大小为 \(5\) 的排列所生成的集合记录下来。

遍历这 \(156\) 个集合,然后按上面的粉色字计算答案即可。


提一嘴搜索的过程。

我们可以将集合中 \(120\) 种排列的选择情况压到一个 \(2^{120}\) 以内的 __int128 里面。

然后从单位元 \(\{[1,2,3,4,5]\}\) 开始 BFS,每次取出一个集合,尝试向里面添加新的元素。

对于新添加的元素,我们将它与集合内其它所有元素复合;对于产生的新元素,我们再进行相同的步骤。直到没有新元素产生为止。

最终我们获得一个可被生成的集合。将这个集合扔进队列,继续 BFS 即可。

点击查看代码
#include<bits/stdc++.h>
#define eb emplace_back
using namespace std;
typedef __int128 B;
typedef unsigned long long ull;
const int N=2e5+5,M=2e5+5,Q=2e5+5;
const B one=1;
namespace Perm{
	map<vector<int>,int> id;//排列对应的编号 
	vector<vector<int>> perm;//5!种排列 
	int prod[120][120];//排列两两复合得到的排列编号 
	inline int pc(B x){return __builtin_popcountll((ull)(x>>64))+__builtin_popcountll((ull)(x&-1ull));}//1的个数 
	inline int lg(B x){return (x>>64)?64+__lg((ull)(x>>64)):__lg((ull)x);}//最高位1的位置 
	struct cmp{
		inline bool operator ()(B u,B v) const{
			return pc(u)>pc(v);//小根堆 
		}
	};
	priority_queue<B,vector<B>,cmp> q;//BFS状态(用优先队列是为了让siz数组递增) 
	map<B,bool> vis;//状态是否访问 
	map<B,int> Id;//状态对应的编号 
	B st[156];//编号对应的状态 
	int siz[156];//某状态所含的排列数量 
	inline vector<int> operator * (const vector<int> &p,const vector<int> &q){
		vector<int> r(5);
		for(int i=0;i<5;i++) r[i]=p[q[i]];
		return r;
	}
	inline B ext(B x){//计算包含x的最小子群 
		B vis=0;
		while(vis^x){
			int i=lg(vis^x);//找到不同的最高位置
			vis|=(one<<i); 
			for(int j=0;j<120;j++)//将新加入的i与x中所有的j复合 
				if((x>>j)&1) x|=(one<<prod[i][j]);
		}
		return x;
	}
	inline void init(){
		vector<int> p(5);
		for(int i=0;i<5;i++) p[i]=i;
		do id[p]=perm.size(),perm.eb(p);
		while(next_permutation(p.begin(),p.end()));
		for(int i=0;i<120;i++)//处理i,j复合的结果 
			for(int j=0;j<120;j++)
				prod[i][j]=id[perm[i]*perm[j]];
		int idx=0;
		q.emplace(1),vis[1]=1;//从单位元[0,1,2,3,4]开始 
		while(q.size()){
			B x=q.top();q.pop();
			st[idx]=x,Id[x]=idx;
			siz[idx++]=pc(x);
			for(int i=0;i<120;i++){
				if(!((x>>i)&1)){//枚举待加入的排列编号 
					B y=ext(x|(one<<i));
					if(!vis[y]) q.emplace(y),vis[y]=1;
				}
			}
		}
	}
}
struct Qry{int u,v;}q[Q];
struct Ed{int u,v,w;}e[M];
int n,m,qc,fa[N],ans[Q];
inline int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
inline void merge(int x,int y){fa[find(x)]=find(y);}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	Perm::init();
	cin>>n>>m>>qc;
	vector<int> w(5);
	for(int i=0,u,v;i<m;i++){
		cin>>u>>v,u--,v--;
		for(int j=0;j<5;j++) cin>>w[j],w[j]--;
		e[i]={u,v,Perm::id[w]};
	}
	for(int i=0;i<qc;i++) cin>>q[i].u>>q[i].v,q[i].u--,q[i].v--;
	for(int _=0;_^156;_++){
		for(int i=0;i<n;i++) fa[i]=i;
		for(int i=0;i<m;i++)
			if((Perm::st[_]>>e[i].w)&1) merge(e[i].u,e[i].v);
		for(int i=0;i<qc;i++)
			if(!ans[i]&&find(q[i].u)==find(q[i].v))
				ans[i]=Perm::siz[_];
	}
	for(int i=0;i<qc;i++)
		if(ans[i]) cout<<ans[i]<<"\n";
		else cout<<"No\n";
	return 0;
}

T4. P14311 【MX-S8-T4】平衡三元组

Ref: 题解:P14311 【MX-S8-T4】平衡三元组 - Zelensky

Test Case #1~2

\(O(n^3)\) 枚举 \(x,y,z\)。时间复杂度 \(O(n^3 q)\)

Test Case #3~5

我们发现 \(y\) 固定时,\(x,z\) 分别取左右区间的最大值是最优的。

因为这在使 \(2A_y\le A_x+A_z\) 更容易成立的同时,也让所求的 \(A_x+A_y+A_z\) 更大。

所以只需要枚举 \(y\) 就够了。时间复杂度 \(O(nq)\)

Solution

\(O(nq)\) 的思路进一步考虑,因为 \(x,z\) 占据了左右区间的 \(\max\),所以 \(x,z\) 中至少一个是整个区间的 \(\max\)

  • 如果 \(y\) 是区间的 \(\max\) 呢?那么为了满足 \(A_x+A_z\ge 2A_y\),必须有 \(A_x=A_z=A_y\),不矛盾。

有了这个结论,我们就可以找到区间 \(\max\),记为 \(m_0\),这样就可以先确定其中一个端点。拿 \(x\) 举例,\(z\) 同理。

接下来,我们找到 \((m_0,r]\) 的最大值,记为 \(m_1\)

image

先考虑 \(y\in (m_0,m_1]\) 的子问题。

  • 若取 \(y\in(m_0,m_1)\),则 \(z=m_1\) 是最优的。而且由于 \(A_x,A_z\ge A_y\),所以 \(A_x+A_z\ge 2A_y\) 恒成立。

  • 若取 \(y=m_1\),则我们找 \((m_1,r]\) 的最大值,记为 \(m_2\)

    image

    • \(z=m_2\) 时合法,则 \(y\) 继续增大也不会让答案更优了,直接结束子问题的求解。

    • \(z=m_2\) 时不合法,则说明 \(y=m_1\) 时取不到合法解。继续递归解决 \(y\in (m_1,m_2]\) 的子问题。

考虑这样做的时间复杂度。

考虑 \(y\in(m_0,m_1]\) 的子问题中,继续递归意味着什么。

意味着 \((x,y,z)=(m_0,m_1,m_2)\) 不合法,也即 \(A_{m_2}<2A_{m_1}-A_{m_0}\)

同理,下一层继续递归 \(\implies A_{m_3}<2A_{m_2}-A_{m_0} \implies A_{m_3}<4A_{m_1}-3A_{m_0}\)

如此归纳可知递归条件为 \(A_{m_i}<2^{i-1}(A_{m_1}-A_{m_0})+A_{m_0}\)

右侧是指数级递减的,所以递归量级是 \(O(\log V)\) 的。

上述运算过程中仅需要支持查询区间 \(\max\) 及其下标,又考虑到有区间加,可以用线段树维护。

总时间 \(O(n\log n\log V)\)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define lc (x<<1)
#define rc (x<<1|1)
#define PII pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=1e6+5,inf=1e18;
int n,q,a[N],ans;
struct SEG{
	int mx[N<<2],p[N<<2],tg[N<<2];
	inline void pushup(int x){
		mx[x]=max(mx[lc],mx[rc]);
		p[x]=(mx[x]==mx[lc]?p[lc]:p[rc]);
	}
	inline void pushdown(int x){
		if(tg[x]){
			mx[lc]+=tg[x],tg[lc]+=tg[x];
			mx[rc]+=tg[x],tg[rc]+=tg[x];
			tg[x]=0;
		}
	}
	inline void build(int x,int l,int r){
		if(l==r) return mx[x]=a[l],p[x]=l,void();
		int mid=(l+r)>>1; 
		build(lc,l,mid),build(rc,mid+1,r),pushup(x);
	}
	inline void chr(int x,int a,int b,int v,int l,int r){
		if(a<=l&&r<=b) return mx[x]+=v,tg[x]+=v,void();
		pushdown(x);
		int mid=(l+r)>>1; 
		if(a<=mid) chr(lc,a,b,v,l,mid);
		if(b>mid) chr(rc,a,b,v,mid+1,r);
		pushup(x);
	}
	inline PII qry(int x,int a,int b,int l,int r){
		if(a<=l&&r<=b) return {mx[x],p[x]};
		pushdown(x);
		int mid=(l+r)>>1;
		if(a>mid) return qry(rc,a,b,mid+1,r);
		if(b<=mid) return qry(lc,a,b,l,mid);
		PII p=qry(lc,a,b,l,mid),q=qry(rc,a,b,mid+1,r);
		return {max(p.fi,q.fi),p.fi>=q.fi?p.se:q.se};
	}
}seg;
inline void solveL(int l,int r,int Mx){
	if(l>=r) return;
	PII t=seg.qry(1,l,r,1,n);
	int mx=t.fi,p=t.se;
	if(p^r) ans=max(ans,seg.qry(1,p+1,r,1,n).fi+Mx+mx);
	if(p==l) return;
	int s=seg.qry(1,l,p-1,1,n).fi;
	if(2*mx<=Mx+s) return ans=max(ans,mx+s+Mx),void();
	solveL(l,p-1,Mx);
}
inline void solveR(int l,int r,int Mx){
	if(l>=r) return;
	PII t=seg.qry(1,l,r,1,n);
	int mx=t.fi,p=t.se;
	if(p^l) ans=max(ans,seg.qry(1,l,p-1,1,n).fi+Mx+mx);//y在(P,p)中 
	if(p==r) return;
	int s=seg.qry(1,p+1,r,1,n).fi;
	if(2*mx<=Mx+s) return ans=max(ans,mx+s+Mx),void();//y=p
	solveR(p+1,r,Mx);
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;i++) cin>>a[i];
	seg.build(1,1,n);
	int op,l,r,v;
	while(q--){
		cin>>op>>l>>r;
		if(op==1){
			ans=-inf;
			PII t=seg.qry(1,l,r,1,n);
			int mx=t.fi,p=t.se;
			solveL(l,p-1,mx);//z取p
			solveR(p+1,r,mx);//x取p
			if(ans==-inf) cout<<"No\n";
			else cout<<ans<<"\n";
		}else cin>>v,seg.chr(1,l,r,v,1,n);
	}
	return 0;
}

  1. here ↩︎

posted @ 2025-10-28 11:55  Sinktank  阅读(32)  评论(0)    收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2025 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.