20251223NOI模拟赛

20251223NOI模拟赛

T2.灯泡控制

题面:

\(n\) 个灯泡摆成一圈,每个有状态开关,初始都是关闭。每次进行操作:

  • 如果一个关闭的灯泡相邻位置有点亮的,则点亮它,对所有灯泡同时进行。

  • 随机一个位置点亮,如果本来就是亮的则无事发生。

求点亮所有灯泡的期望操作轮数。 \(n\leq 10000\)

题解:

发现操作灯泡变量的个数和具体位置无关,和连续段有关,所以考虑连续段 DP。

对于环上连续段,由于开头结尾相邻,合并连续段时,连续段之间的前后位置关系不好处理,容易算重。有技巧之记录的连续段 \(A,B\) 是无序的,直到 \(A,B\) 合成为 \(C\) 时再决定 \(C=A+B\)\(C=B+A\)。这样就不用管环的问题。

将操作分为三步:向左扩展,向右扩展,随机点亮。

\(f_{i,j},g_{i,j}\) 分别表示当前点亮 \(i\) 盏灯,形成 \(j\) 个连续段的期望和概率。

考虑扩展操作:

预处理 \(w_{i,j}\) 表示将 \(i\) 个段合成 \(j\) 个段的方案数。

  • 转移考虑最后一个段自成一段 \(w_{i-1,j-1}\to w_{i,j}\)

  • 或者考虑最后一个段合成到前面某个段。如果它合成到一个由原来的 \(a\) 个段合并成的新段中,考虑此时决定相对位置有 \(a+1\) 种情况,所以一共有 \(i-1+j\) 种情况。即 \(w_{i-1,j}\times (i-1+j)\to w_{i,j}\)

考虑随机点亮操作:

  • 点亮自成一段 \((i-1,j-1)\to(i,j)\)

  • 延长一段 \((i-1,j)\times 2j\to(i,j)\)

  • 合并一段 \((i-1,j+1)\times j(j+1)\to(i,j)\)。注意因为段在合并前无序,所以排列数随意选择两个合并。

  • 无事发生 \((i,j)\times i\to(i,j)\)

注意由于是环所以点亮前的最后一步操作要枚举。

连续段个数不超过 \(\sqrt n\),所以复杂度 \(O(n^2)\)

代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;

inline int read(){
	int s=0,k=1;
	char c=getchar();
	while(c>'9'||c<'0'){
		if(c=='-') k=-1;
		c=getchar();
	}
	while(c>='0'&&c<='9'){
		s=(s<<3)+(s<<1)+(c^48);
		c=getchar();
	}
	return s*k;
}

const int N=1e4+5,M=105,mod=998244353;
int n,m;
ll f[3][N][M],g[3][N][M],w[M][M],inc;

ll Mod(ll x){return x>=mod?x-mod:x;}
void Add(ll &x,ll y){x=Mod(x+y);}

ll ksm(ll a,ll b){
	ll t=1;
	for(;b;b>>=1,a=a*a%mod)
		if(b&1) t=t*a%mod;
	return t;
}

int main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read();m=sqrt(n);
	if(n==1){
		puts("1");
		return 0;
	}
	inc=ksm(n,mod-2);
	w[0][0]=1;
	for(int i=1;i<=m;i++)
		for(int j=1;j<=i;j++)
			w[i][j]=(w[i-1][j-1]+w[i-1][j]*(i+j-1))%mod;
	f[0][1][1]=1;g[0][1][1]=1;
	for(int i=2;i<=n;i++){
		for(int o=1;o<=2;o++)
			for(int j=1;j<=i&&j<=m;j++)	
				for(int k=j;k<i&&k<=m;k++){
					(f[o][i][j]+=f[o-1][i-k][k]*w[k][j])%=mod;
					(g[o][i][j]+=g[o-1][i-k][k]*w[k][j])%=mod;
				}
		for(int j=1;j<=i&&j<=m;j++){
			(f[0][i][j]+=f[2][i-1][j-1]+f[2][i-1][j]*2*j+f[2][i-1][j+1]*j*(j+1)+f[2][i][j]*i)%=mod;
			(g[0][i][j]+=g[2][i-1][j-1]+g[2][i-1][j]*2*j+g[2][i-1][j+1]*j*(j+1)+g[2][i][j]*i)%=mod;
			(f[0][i][j]*=inc)%=mod;
			(g[0][i][j]*=inc)%=mod;
			Add(f[0][i][j],g[0][i][j]);
		}
	}
	ll ans=0;
	(ans+=Mod(f[2][n-1][1]+g[2][n-1][1])*inc)%=mod;
	w[0][1]=1;
	for(int i=1;i<=m;i++)
		(ans+=(f[0][n-i][i]+f[1][n-i][i]+g[0][n-i][i]+g[1][n-i][i])%mod*w[i-1][1]%mod)%=mod;
	printf("%lld",ans);
	return 0;
}

T3.树上的数

题面:

给定一个 \(n\) 个点的树,初始每个点有点权 \(a_i\)。每次操作你可以选择一个节点 \(x\),将 \(a_x\) 赋值为其邻域点权和减 \(a_x\),并将其邻域点权全部赋为 \(0\)。执行任意次操作后使得最终只剩下一个非 \(0\) 点时求这个点的最大值。\(n\leq 10^6,-10^9\leq a_i\leq 10^9\)

题解:

每个点对最终答案贡献系数为 \(1\)\(-1\)

设初始未合并的点为 \(A\) 类点,涉及过合并操作的点为 \(B\) 类点。

发现 \(B\) 类点可以任意更改正负,且可以在其经过的路径上任意搬运。

进一步的,当场上存在 \(A\) 类点时,每次合并操作最多涉及一个 \(B\) 类点,直到都是 \(B\) 类点时,由于上述性质,答案为所有 \(B\) 类点绝对值之和。任意操作方案都可以转化为这样的操作方案一定不劣。

结论是问题可以转化为:将树的若干边割断变成森林,对于森林中每个树选择一个根,要求任意两棵树的根在原树上不相邻。还要求每棵树以选择点为根时,兄弟节点贡献系数相同,根节点与其儿子系数不同。

对于一个操作方案,直到所有点都是 \(B\) 类点之前的合并过程中,将其合并操作不涉及到的所有边切割,得到一个森林,显然一个森林始终有一个 \(B\) 类点。将其第一次操作的位置作为根,两根不能相邻。由根不断向外扩展吸收 \(A\) 类点,每次所有兄弟会一起被合并,所以兄弟节点系数相同。

对于一个切割方案,如果一个切割边两边都不是根那显然可以做到两边互不影响,否则如果有一边是根则必须先合并另一边才能操作这个根。对于所有有一边是根的两个连通块之间连一条有向边,根据拓扑序操作每个联通块,可以达到最终效果。

考虑树形 DP,对每个 \(x\) 记录状态:

  • 其到父亲的边被切割,且该节点不是根节点,子树内的最大贡献

  • 其到父亲的边被切割,且该节点是根节点,子树内的最大贡献;

  • 其到父亲的边未被切割,且根节点在子树外,子树内(自身除外)的最大贡献;

  • 其到父亲的边未被切割,且根节点在子树内,子树内以及父亲节点的最大贡献;

转移时考虑枚举本层兄弟节点那个相同的系数是 \(1/-1\),考虑根的位置(父亲,子树,自己)转移即可。复杂度 \(O(n)\)

代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;

inline ll read(){
	ll s=0,k=1;
	char c=getchar();
	while(c>'9'||c<'0'){
		if(c=='-') k=-1;
		c=getchar();
	}
	while(c>='0'&&c<='9'){
		s=(s<<3)+(s<<1)+(c^48);
		c=getchar();
	}
	return s*k;
}

void write(ll x){
	if(x>=10) write(x/10);
	putchar('0'+x%10);
}

const int N=1e6+5;
const ll inf=1e18+7;
int n,fat[N];
ll a[N],f[N][4];
vector<int>e[N];

void dfs(int x){
	if(e[x].size()==0){
		f[x][0]=-inf;
		f[x][1]=llabs(a[x]);
		f[x][2]=0;
		f[x][3]=llabs(a[x]-a[fat[x]]);
		return ;
	}
	for(int v:e[x]) dfs(v);
	ll sum[2]={0,0},mx[2]={-inf,-inf};
	for(int v:e[x]){
		ll val;
		val=max(a[v]+f[v][2],max(f[v][0],f[v][1]));
		sum[0]+=val; mx[0]=max(mx[0],f[v][3]-val);
		val=max(-a[v]+f[v][2],max(f[v][0],f[v][1]));
		sum[1]+=val; mx[1]=max(mx[1],f[v][3]-val);
	}
	f[x][0]=max(sum[0]+mx[0],sum[1]+mx[1]);
	f[x][2]=max(sum[0],sum[1]);
	f[x][3]=max(sum[0]+mx[0]+a[fat[x]],sum[1]+mx[1]-a[fat[x]]);
	sum[0]=sum[1]=0;
	for(int v:e[x]){
		sum[0]+=max(a[v]+f[v][2],f[v][0]);
		sum[1]+=max(-a[v]+f[v][2],f[v][0]);
	}
	f[x][1]=max(sum[0]-a[x],sum[1]+a[x]);
	f[x][3]=max({f[x][3],sum[0]+a[fat[x]]-a[x],sum[1]-a[fat[x]]+a[x]});
}

void solve(){
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=2;i<=n;i++){
		fat[i]=read();
		e[fat[i]].push_back(i);
	}
	dfs(1);
	write(max(f[1][0],f[1][1]));putchar('\n');
	for(int i=1;i<=n;i++) e[i].clear();
}

int main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	int ID=read(),T=read();
	while(T--) solve();
	return 0;
}

posted @ 2025-12-24 08:42  programmingysx  阅读(16)  评论(0)    收藏  举报
Title