2023.8.3测试

暑假NOIP模拟赛-1

一场 NOIP 模拟赛搬了四道 Atcoder 的题

T1 跑路

一个 n×m01 矩阵 A,从左上角出发,每次向下走或向右走,终点为右下角。若路径途中只经过 0,则称 A 为“好矩阵”。给定矩阵 A,每次可以选择它的一个子矩阵取反,求将 A 变成“好矩阵”的最小操作数。

1n,m1000

签到题,设 f[x][y] 表示从 (x,y) 出发到终点的最小操作数是多少。显然操作数与路径上 1 的连续段个数一一对应,记忆化搜索即可

T2 Cleaning

一开始只会打特殊性质,最后 20min 才打出正解

数据太水放我过了,但实际上在 At 过不了(细节出大锅)

对于节点 x,设从儿子节点传上来的石头数量总和为 s=yson(x)vy,显然这些石头可以在此处合并,也可以往上走。设它们的数量分别为 a,b,则有方程

{2a+b=sa+b=vx

解得

{a=svxb=2vxs

显然有范围限制 0a,bvx

对于在此处合并的,考虑存在方案使得可以凑出 a 次合并的条件。一顿模拟后,发现只要 smaxyson(x)vya 即可

特别地,当 x=rt 时,要有 a=vx,b=0

注意特判 n=2!!!!!!

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

const int N=100010,M=400010,INF=1e9;

int T,n,rt,a[N],deg[N];
vector <int> g[N];
bool flag1,flag2;

bool dfs(int x,int fa)
{
	if(deg[x]==1)
		return 1;			
	
	LL sum=0,mx=0;
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		if(y==fa)
			continue;
		bool ansy=dfs(y,x);
		if(!ansy)
			return 0;
		sum+=(LL)a[y];
		mx=max(mx,(LL)a[y]);
	}
	
	if(sum<(LL)a[x])
		return 0;
	LL aa=sum-(LL)a[x],bb=1LL*2*a[x]-sum;
	if(aa<0 || bb<0 || bb>a[x] || aa>a[x] || sum-mx<aa)
		return 0;
	if(x==rt && aa!=a[x])
		return 0;
	a[x]-=aa;
	return 1;
}

int main()
{
	scanf("%d",&n);
	for(int i=1; i<=n; i++)
		scanf("%d",&a[i]);
	for(int i=1; i<n; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		deg[x]++;  deg[y]++;
		g[x].push_back(y);  g[y].push_back(x);
	}
		
	if(n==2)	
	{
		if(a[1]==a[2])
			printf("YES");
		else
			printf("NO");
		return 0;
	}		
		
	for(int i=1; i<=n; i++)
	{
		if(deg[i]!=1)
		{
			rt=i;
			break;
		}
	}
		
	bool pd=dfs(rt,0);
	if(pd)
		printf("YES");
	else
		printf("NO");

	return 0;
}

T3 Two Faced Edges

很牛逼的题,只会暴力 Tarjan,喜提 40

先求一次 SCC。对于边 (x,y),分情况讨论:

  • xy 在同一个 SCC

    此时 SCC 数量若不改变,则需满足还存在一条 xy 的路径不经过边 (x,y)

  • xy 不在同一个 SCC

    此时 SCC 数量若改变,则同样需满足还存在一条 xy 的路径,不经过边 (x,y)

g[x][y] 表示是否存在 xy 的路径且不含边 (x,y),如何计算?

x 出发对整张图进行一次深搜,再将边表反过来重新搜一次。若 xdfs 树中的深度为 1,则 g[x][y]=0 的充分条件为两次搜索中 y 的深度都为 2

时间复杂度 O(nm)

#include<bits/stdc++.h>
using namespace std;

const int N=1010,M=200010;

int n,m;
int cnt,dfn[N],low[N],num,ins[N],c[N],sta[N],top;
int f[N][N],dep[N],cmp[N];
struct node{int x,y;}e[M];
vector <int> g[N];

void Tarjan(int x)
{
	dfn[x]=low[x]=++num;
	sta[++top]=x;  ins[x]=1;
	int len=g[x].size();
	for(int i=0; i<len; i++)
	{
		int y=g[x][i];
		if(!dfn[y])
		{
			Tarjan(y);
			low[x]=min(low[x],low[y]);
		}
		else if(ins[y])
			low[x]=min(low[x],dfn[y]);
	}
	
	if(dfn[x]==low[x])
	{
		int y;  cnt++;
		do
		{
			y=sta[top--];
			ins[y]=0;
			c[y]=cnt;
		}while(x!=y);
	}
}

void dfs1(int x,int d)
{
	dep[x]=d;
	int len=g[x].size();
	for(int i=0; i<len; i++)
	{
		int y=g[x][i];
		if(dep[y])
			continue;
		dfs1(y,d+1);
	}
}

void dfs2(int x,int d)
{
	dep[x]=d;
	int len=g[x].size();
	for(int i=len-1; i>=0; i--)
	{
		int y=g[x][i];
		if(dep[y])
			continue;
		dfs2(y,d+1);
	}
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1; i<=m; i++)
	{
		scanf("%d%d",&e[i].x,&e[i].y);
		g[e[i].x].push_back(e[i].y);
	}
	
	for(int i=1; i<=n; i++)
		if(!dfn[i])
			Tarjan(i);
	
	for(int i=1; i<=n; i++)
	{
		for(int j=1; j<=n; j++)
			dep[j]=0;
		dfs1(i,1);
		for(int j=1; j<=n; j++)
			cmp[j]=dep[j],dep[j]=0; 
		dfs2(i,1);
		for(int j=1; j<=n; j++)
		{
			if(cmp[j]==2 && dep[j]==2)
				f[i][j]=0;
			else
				f[i][j]=1;
			
		}
	}
	
	for(int i=1; i<=m; i++)
	{
		int x=e[i].x,y=e[i].y;
		if(c[x]==c[y])
		{
			if(f[x][y])
				printf("same\n");
			else
				printf("diff\n");
		}
		else
		{
			if(f[x][y])
				printf("diff\n");
			else
				printf("same\n");
		}
	}
	
	return 0;
}

T4 ColoringBalls

神仙题,不在能力范围之内

Step 1:考虑某种颜色序列能否被生成

注意到如果一些颜色段之间有黑色,那么这些颜色段之间就是相互独立的。于是我们先考虑这些独立的颜色段

一些叠加的等效的操作(如操作 rbr 染成一个纯红色段)其实都可以变成一个最简的操作(只操作 r,略过 rb),因此我们我们定义一个颜色序列 C 的最简操作序列 P 为: P 能染出 CP 的任意子序列都无法染出 C。可以定义纯红段为只有红色的独立颜色段,杂色段为红蓝混合的独立颜色段

  • 考虑杂色段的最简操作序列

    不难发现,如果一个杂色段段有 k 个蓝色段,那么至少要染 k+1 次,红色和蓝色的染色次数在 [1,k] 之间,都至少染一次。特殊的,若没有蓝色,则只需染一次红色

    在此基础上,还有染色顺序的限制。若我们决定染 a 次蓝色,最好的方案是:先染 1 次红色,再染 a1 次蓝色,然后染一大段蓝色,最后这段大蓝色染完剩下的红色

    因此对杂色段最简操作序列的要求:满足个数 =k+1,最前面是 r,第二个是 b

  • 而纯红段的最简操作序列即是一个 r

接着考虑由黑球分隔的各个颜色段

  • 设有 c 个杂色段,现在要在整个操作序列中选出若干个子序列,分别作为每个杂色段的操作序列。

    我们先要选出 c 个子序列 {r,b} 作为 c 个最简操作序列的开头,之后要为每个最简操作序列添加指定数目的字符,且满足字符在开头 {r,b} 的右侧。

    ti 表示表示第 i{r,b} 右侧剩余的未选择的字符数目,对应的最简操作序列还需要 si 个字符。那贪心一下,显然我们要将 ti 从大到小排序,依次考虑,每次要尽量靠左选择字符

    形式化的,有解的充要条件是

    i,tij=icsi

    用人话说就是剩余的字符数目要够自己和后面的所有 {r,b} 选择

    进一步考虑如何排列 s 的顺序。不难发现,将 si 降序排列,每个 j=icsj 都取到最小值,此时最可能满足条件

  • 再考虑 a 个纯红色段的情况,根据贪心,显然选取前面 ar 最优


Step 2:DP 计数

枚举纯红段和杂色段的数量 a,c,可确定 t1c

先考虑在确定 s 的情况下如何确定颜色序列的方案数

  • 先考虑颜色段之间的排列顺序。首先是杂色段和纯红段混合的方案数,为 (a+ca)。由于纯红段最简操作序列的长度都是 1,因此我们只需考虑杂色段内部的顺序数,即 s 的不同排列数。这是典型的多重排列,记 xi=[sj=i],排列数即为 (si)!ci!

  • 接下来考虑确定颜色段顺序后的方案数

    各个颜色段长度不定,可以看作装球的盒子

    • 对于一个杂色段对应的 si,(注意总字符数为 si+2,蓝色段个数为 si+1),会产生 2si+1 个至少有一个球的颜色段,以及 2 个可以为空的颜色段(两端的红球段)

    • 对于一个纯红段,会产生 1 个至少有一个球的颜色段

    • 对于黑色的部分,会产生 a+c1 个至少有一个球的颜色段,以及 2 个可以为空的颜色段(两端的黑球段)

    综上,非空盒的个数为 2(si+c+a)1,可空盒的个数为 2c+2,总球数为 n,插板法可求,答案为

    (n+2c+21(si+c+a)1+2c+21)

接下来对不同的 s 序列进行统计。

f[i][j][k] 表示填写了 sic,目前 s=j,且 si=k 的方案数,则有转移:

f[i][j][k]=p1(ci+1k)g<kf[i+p][jpk][g]

式子 (ci+1k) 用于处理多重排列,是将 pk 归并到目前的 s 的方案数

前文的 i,tij=icsi 在这里体现为要求 jtijpkti+p

最终的答案为 (a+ca)×(f[1][_][_]×cnt)cnts 序列的不同排列数,根据第二维的 s 求出

使用前缀和优化 dp 转移,再加上枚举 a,c 的复杂度,时间复杂度 O(n5logn),可以通过

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

const int N=80,MOD=1e9+7;

int n,m;
int C[6*N][6*N],t[N],posr[N],posb[N];
LL ans,f[N][N][N];;
char str[N];
bool v[N];

void prework()
{
	for(int i=0; i<=320; i++)
		C[i][0]=1;
	for(int i=1; i<=320; i++)
		for(int j=1; j<=i; j++)
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%MOD;
}

LL calc(int a,int c)
{
	memset(v,0,sizeof(v));
	memset(f,0,sizeof(f));
	
	int cnt=0;  LL res=0;
	for(int i=1; i<=m; i++) //计算杂色段的{r,b}
	{
		if(cnt==c)
			break;
		if(str[i]=='r')
		{
			v[i]=1;
			posr[++cnt]=i;  posb[cnt]=0;
			for(int j=i+1; j<=m; j++)
			{
				if(str[j]=='b' && !v[j])
				{
					v[j]=1;
					posb[cnt]=j;
					break;
				}
			}
			if(!posb[cnt])
				return 0;
		}
	}
	if(cnt<c)
		return 0;
	
	cnt=0;
	for(int i=1; i<=m; i++) //计算纯红段的一个r
	{
		if(cnt==a)
			break;
		if(v[i])
			continue;
		if(str[i]=='r')
			v[i]=1,cnt++;
	}
	if(cnt<a)	
		return 0;
	
	cnt=0;  int id=c;
	for(int i=m; i>=1; i--)  //计算t_1~t_c
	{
		if(!v[i])
			cnt++;
		if(i==posb[id])
			t[id]=cnt,id--;
	}
	
	for(int i=0; i<=m; i++)	
		f[c+1][0][i]=1;
	for(int i=c; i>=1; i--)
	{
		for(int j=0; j<=t[i] && 2*(j+a+c)-1<=n; j++)
		{
			for(int k=1; k<=m; k++)
			{
				int kk=k-1; //注意这里有一个下标的偏移
				for(int p=1; p<=c-i+1 && j-p*kk>=0 && j-p*kk<=t[i+p]; p++)
					(f[i][j][k]+=1LL*f[i+p][j-p*kk][k-1]*C[c-i+1][p]%MOD)%=MOD;
			}
			
			for(int k=1; k<=m; k++)  //前缀和优化
				(f[i][j][k]+=f[i][j][k-1])%=MOD;
		}
	}
	
	for(int j=0; j<=m; j++)
	{
		int x=2*(j+a+c)-1,y=2*c+2;
		(res+=1LL*f[1][j][m]*C[n+y-1][x+y-1]%MOD)%=MOD;  //s序列的不同排列数
	}
	res=1LL*res*C[a+c][a]%MOD;  //别忘了这个
	
	return res;	
}

int main()
{
	prework();
	
	scanf("%d%d%s",&n,&m,str+1);
	
	for(int a=0; a<=m; a++)  //枚举a,c
		for(int c=0; a+2*c<=m; c++)
			(ans+=calc(a,c))%=MOD;
			
	printf("%lld",ans);

	return 0;
}

100+100+40+0=240rk6

总结

  • T1 切的有点慢,以后得加快速度

  • T2 最后时间才想出正解,需要加强思维。部分分要大胆写,这次正解就是在部分分里想出来的。写代码要注意细节与范围限制,不能写的太随便

  • T3 其实可以进一步思考,不能满足暴力

  • T4 随缘

posted @   xishanmeigao  阅读(8)  评论(0编辑  收藏  举报
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示