8.21 训练总结

前言

今天花了一天时间配交互库,然后各种调整,反正今天挺水的。

A. Rotating Mole

题面

(本来是前天打的公开赛,但是前天没写所以就挪到今天来了)。

简单题,看错题了还吃了两发罚时。

显然如果两个点在 \(1\) 号点的两棵不同的子树内,那么这两个点的最短路就是 \(1\)。

如果不在,那么其中一个点可以先走到 \(1\),再从 \(1\) 走到另一个节点,此时最短路是 \(2\)。

于是就成简单题了。

代码:

const int N=1e6+6;
int n,ans,siz[N];
vector<int>v[N];
void dfs(int x,int fa)
{
	siz[x]=1;
	for(auto i:v[x])
	{
		if(i==fa)
		{
			continue;
		}
		dfs(i,x);
		siz[x]+=siz[i];
	}
}
signed main()
{
    n=read();
	for(int i=2,x;i<=n;i++)
	{
		x=read();
		v[x].push_back(i);
		v[i].push_back(x);
	}
	dfs(1,0);
	int sum=siz[1]*(siz[1]-1);
	for(auto i:v[1])
	{
		sum-=siz[i]*(siz[i]-1);
	}
	ans+=n*(n-1)*2-sum;
	write(ans);
	putchar('\n');
	return 0;
}

B. Gift

题面

瞪结论题。

首先因为点数等于边数,而且每个点最多只会和另一个点连一条边,所以我们可以瞪出来这个东西的每个连通块只有两种形态:

  1. 树:此时结论显然是树上度最大的点的度数加一,这个可以瞪出来。

  2. 基环树:我们单独考虑一下。

基环树本质上就是一个环上挂了一堆树,那那些树的答案显然跟上面的计算方法一样,我们考虑中间的环怎么做。

手玩几组样例你会发现:如果环长小于等于 \(5\),那么答案就是环长,否则根据环长除以 \(3\) 后的余数可以得出结论:当余数为 \(0\) 时,答案是 \(3\),否则答案是 \(4\)。那么最终答案就是在所有答案里面取最大值。

代码:

const int N=5e5+6;
int n,d[N];
bool vis[N],b[N];
vector<int>v[N];
signed main()
{
    int T=read();
	while(T--)
	{
		n=read();
		for(int i=1,x;i<=n;i++)
		{
			x=read();
			if(i==x)
			{
				continue;
			}
			d[i]++,d[x]++;
			v[x].push_back(i);
			v[i].push_back(x);
		}
		int ans=0;
		for(int i=1;i<=n;i++)
		{
			if(vis[i])
			{
				continue;
			}
			vector<int>vv;
			vv.push_back(i);
			vis[i]=1;
			int mx=0,cnt=0;
			for(int j=0;j<vv.size();j++)
			{
				mx=max(mx,d[vv[j]]);
				cnt+=d[vv[j]];
				for(auto k:v[vv[j]])
				{
					if(vis[k])
					{
						continue;
					}
					vis[k]=1;
					vv.push_back(k);
				}
			}
			int V=vv.size(),E=cnt/2;
			int res=0;
			if(V-1==E)
			{
				res=mx+1;
			}
			else
			{
				queue<int>q;
				for(auto j:vv)
				{
					if(d[j]<=1)
					{
						q.push(j);
					}
				}
				while(!q.empty())
				{
					int u=q.front();
					q.pop();
					if(b[u])
					{
						continue;
					}
					b[u]=1;
					for(auto j:v[u])
					{
						if(!b[j])
						{
							d[j]--;
							if(d[j]==1)
							{
								q.push(j);
							}
						}
					}
				}
				int len=0;
				for(auto j:vv)
				{
					if(!b[j])
					{
						len++;
					}
				}
				res=mx+1;
				if(len<=5)
				{
					res=max(res,len);
				}
				else
				{
					res=max(res,(len%3?4ll:3ll));
				}
			}
			ans=max(ans,res);
		}
		write(ans);
		putchar('\n');
		for(int i=1;i<=n;i++)
		{
			d[i]=vis[i]=b[i]=0;
			v[i].clear();
		}
	}
	return 0;
}

然后我就只看了前两题。

C. ZUMA

题面

一眼看出来是 DP。

我先设了个形似 \(f_i\) 的 DP,尝试转移发现没法转移。但是我们会观察到转移的过程很像把两段区间拼在一起,结合 \(n\le100\) 的数据范围,我们考虑区间 DP。

设 \(f_{i,j}\) 表示将区间 \([i,j]\) 之内的所有数删完需要添加的最少子弹数,最显然的转移就是 \(f_{i,j}=\min_{i\le k\lt j}\{f_{i,k}+f_{k+1,j}\}\),但是你会发现一个很大的问题:如果左右两边的合在一起删除怎么办?显然此时我们可以用更少的子弹数解决问题。

我们很容易想到这个问题根本不需要考虑这个区间内每一个相同颜色之间的操作,因为这个显然可以通过更短的区间来计算出来,因此我们每次只需要考虑区间两端的颜色。

因此我们设 \(g_{i,j,l}\) 表示考虑区间 \([i,j]\),在 \(c_i=c_j\) 的前提下,我们在区间中保留了 \(l\) 个跟 \(c_i\) 颜色相同的子弹,此时还需要补充的最少子弹数是多少。

(定义有点长慢慢看)。

那么显然我们还有一种转移就是 \(f_{i,j}=\min_{2\le l\le k}\{g_{i,j,l}\}\)。

我们考虑 \(g\) 怎么转移,显然有 \(g_{i,j,l}=\min_{i\le x\lt j\land c_x=c_i}\{g_{i,x,l-1}-(k-(l-1))+(k-l)+f_{x+1,j-1}\}\),\(-(k-(l-1))\) 是为了删除 \([i,x]\) 间需要的子弹数,\(+(k-l)\) 是为了计算当前区间需要的子弹数。

当然理论上讲 \(l\) 是没有严格上限的,但是因为当 \(l\ge k\) 时可以视作 \(l=k\),所以第三维可以压缩成 \(O(k)\) 的。

总时间复杂度:\(O(n^3k)\)。

代码:

const int N=106,K=6;
int n,k,a[N],f[N][N],g[N][N][K];
signed main()
{
    n=read(),k=read();
	read(a+1,a+n+1);
	for(int i=1;i<=n;i++)
	{
		for(int j=i;j<=n;j++)
		{
			f[i][j]=1e18;
			for(int l=2;l<=k;l++)
			{
				g[i][j][l]=1e18;
			}
		}
	}
	for(int i=1;i<=n;i++)
	{
		f[i][i]=k-1;
	}
	for(int len=2;len<=n;len++)
	{
		for(int i=1;i+len-1<=n;i++)
		{
			int j=i+len-1;
			for(int l=i;l<j;l++)
			{
				f[i][j]=min(f[i][l]+f[l+1][j],f[i][j]);
				if(a[i]==a[j]&&a[i]==a[l])
				{
					for(int p=3;p<=k;p++)
					{
						g[i][j][p]=min(max(g[i][l][p-1]+f[l+1][j-1]-1,0ll),g[i][j][p]);
					}
					g[i][j][k]=min(g[i][j][k],g[i][l][k]+f[l+1][j-1]);//单独转移
				}
			}
			if(a[i]==a[j])
			{
				g[i][j][2]=min(g[i][j][2],f[i+1][j-1]+k-2);
			}
			for(int l=2;l<=k;l++)
			{
				f[i][j]=min(f[i][j],g[i][j][l]);
			}
		}
	}
	write(f[1][n]);
	putchar('\n');
	return 0;
}

D. OR-XOR

题面

一眼贪心(别问为什么,问就是这种题见多了)。

显然为了最终答案最小,我们需要从高往低枚举二进制位,然后一次判断当前这一位是否可以是 \(0\)。

显然我们可以将 \(n\) 个数尽量分段,然后数段的个数。为了让低位的段和高位的段不冲突,我们可以记录当前不能分段的位置有哪些。

然后就做完了。

纯水题。

代码:

const int N=5e5+6;
int n,m,a[N];
bool b[N];
signed main()
{
	n=read(),m=read();
	read(a+1,a+n+1);
	int ans=0;
	for(int s=62;s>=0;s--)
	{
		int sum=0,cnt=0;
		for(int i=1;i<=n;i++)
		{
			sum^=((a[i]>>s)&1);
			if(!sum&&!b[i])
			{
				cnt++;
			}
		}
		if(cnt<m||sum)
		{
			ans+=(1ll<<s);
		}
		else
		{
			for(int i=1;i<=n;i++)
			{
				sum^=((a[i]>>s)&1);
				if(sum&&!b[i])
				{
					b[i]=1;
				}
			}
		}
	}
	write(ans);
	putchar('\n');
	return 0;
}
posted @ 2026-08-21 22:00  cwxcplh  阅读(8)  评论(0)    收藏  举报