一场 \(\rm NOIP\) 模拟赛搬了四道 Atcoder 的题
T1 跑路
一个 \(n\times m\) 的 \(01\) 矩阵 \(A\),从左上角出发,每次向下走或向右走,终点为右下角。若路径途中只经过 \(0\),则称 \(A\) 为“好矩阵”。给定矩阵 \(A\),每次可以选择它的一个子矩阵取反,求将 \(A\) 变成“好矩阵”的最小操作数。
\(1\leq n,m\leq 1000\)
签到题,设 \(f[x][y]\) 表示从 \((x,y)\) 出发到终点的最小操作数是多少。显然操作数与路径上 \(1\) 的连续段个数一一对应,记忆化搜索即可
T2 Cleaning
一开始只会打特殊性质,最后 \(20\rm min\) 才打出正解
数据太水放我过了,但实际上在 At 过不了(细节出大锅)
对于节点 \(x\),设从儿子节点传上来的石头数量总和为 \(s=\sum\limits_{y\in son(x)}v_y\),显然这些石头可以在此处合并,也可以往上走。设它们的数量分别为 \(a,b\),则有方程
\[\begin{cases}2a+b=s\\a+b=v_x\end{cases} \]解得
\[\begin{cases}a=s-v_x\\b=2v_x-s\end{cases} \]显然有范围限制 \(0\leq a,b\leq v_x\)
对于在此处合并的,考虑存在方案使得可以凑出 \(a\) 次合并的条件。一顿模拟后,发现只要 \(s-\max\limits_{y\in son(x)}v_y\leq a\) 即可
特别地,当 \(x=rt\) 时,要有 \(a=v_x,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
很牛逼的题,只会暴力 \(\rm Tarjan\),喜提 \(40\) 分
先求一次 \(\rm SCC\)。对于边 \((x,y)\),分情况讨论:
-
\(x\) 和 \(y\) 在同一个 \(\rm SCC\) 里
此时 \(\rm SCC\) 数量若不改变,则需满足还存在一条 \(x\rightarrow y\) 的路径不经过边 \((x,y)\)
-
\(x\) 和 \(y\) 不在同一个 \(\rm SCC\) 里
此时 \(\rm SCC\) 数量若改变,则同样需满足还存在一条 \(x\rightarrow y\) 的路径,不经过边 \((x,y)\)
设 \(g[x][y]\) 表示是否存在 \(x\rightarrow y\) 的路径且不含边 \((x,y)\),如何计算?
从 \(x\) 出发对整张图进行一次深搜,再将边表反过来重新搜一次。若 \(x\) 在 \(\rm dfs\) 树中的深度为 \(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\) 能染出 \(C\) 而 \(P\) 的任意子序列都无法染出 \(C\)。可以定义纯红段为只有红色的独立颜色段,杂色段为红蓝混合的独立颜色段
-
考虑杂色段的最简操作序列
不难发现,如果一个杂色段段有 \(k\) 个蓝色段,那么至少要染 \(k+1\) 次,红色和蓝色的染色次数在 \([1,k]\) 之间,都至少染一次。特殊的,若没有蓝色,则只需染一次红色
在此基础上,还有染色顺序的限制。若我们决定染 \(a\) 次蓝色,最好的方案是:先染 \(1\) 次红色,再染 \(a-1\) 次蓝色,然后染一大段蓝色,最后这段大蓝色染完剩下的红色
因此对杂色段最简操作序列的要求:满足个数 \(=k+1\),最前面是
r
,第二个是b
; -
而纯红段的最简操作序列即是一个
r
接着考虑由黑球分隔的各个颜色段
-
设有 \(c\) 个杂色段,现在要在整个操作序列中选出若干个子序列,分别作为每个杂色段的操作序列。
我们先要选出 \(c\) 个子序列
{r,b}
作为 \(c\) 个最简操作序列的开头,之后要为每个最简操作序列添加指定数目的字符,且满足字符在开头{r,b}
的右侧。记 \(t_i\) 表示表示第 \(i\) 个
{r,b}
右侧剩余的未选择的字符数目,对应的最简操作序列还需要 \(s_i\) 个字符。那贪心一下,显然我们要将 \(t_i\) 从大到小排序,依次考虑,每次要尽量靠左选择字符形式化的,有解的充要条件是
\[\forall i,t_i\geq\sum_{j=i}^c{s_i} \]用人话说就是剩余的字符数目要够自己和后面的所有
{r,b}
选择进一步考虑如何排列 \(s\) 的顺序。不难发现,将 \(s_i\) 降序排列,每个 \(\sum\limits_{j=i}^cs_j\) 都取到最小值,此时最可能满足条件
-
再考虑 \(a\) 个纯红色段的情况,根据贪心,显然选取前面 \(a\) 个
r
最优
Step 2:\(\rm DP\) 计数
枚举纯红段和杂色段的数量 \(a,c\),可确定 \(t_{1\sim c}\)
先考虑在确定 \(s\) 的情况下如何确定颜色序列的方案数
-
先考虑颜色段之间的排列顺序。首先是杂色段和纯红段混合的方案数,为 \(\dbinom{a+c}{a}\)。由于纯红段最简操作序列的长度都是 \(1\),因此我们只需考虑杂色段内部的顺序数,即 \(s\) 的不同排列数。这是典型的多重排列,记 \(x_i=[s_j=i]\),排列数即为 \(\dfrac{(\sum{s_i})!}{\prod{c_i}!}\)
-
接下来考虑确定颜色段顺序后的方案数
各个颜色段长度不定,可以看作装球的盒子
-
对于一个杂色段对应的 \(s_i\),(注意总字符数为 \(s_i+2\),蓝色段个数为 \(s_i+1\)),会产生 \(2s_i+1\) 个至少有一个球的颜色段,以及 \(2\) 个可以为空的颜色段(两端的红球段)
-
对于一个纯红段,会产生 \(1\) 个至少有一个球的颜色段
-
对于黑色的部分,会产生 \(a+c-1\) 个至少有一个球的颜色段,以及 \(2\) 个可以为空的颜色段(两端的黑球段)
综上,非空盒的个数为 \(2(\sum{s_i}+c+a)-1\),可空盒的个数为 \(2c+2\),总球数为 \(n\),插板法可求,答案为
\[\binom{n+2c+2-1}{(\sum{s_i}+c+a)-1+2c+2-1} \] -
接下来对不同的 \(s\) 序列进行统计。
设 \(f[i][j][k]\) 表示填写了 \(s_{i\sim c}\),目前 \(\sum s=j\),且 \(s_i=k\) 的方案数,则有转移:
\[f[i][j][k]=\sum_{p\geq 1}{\binom{c-i+1}{k}\sum_{g<k}{f[i+p][j-pk][g]}} \]式子 \(\binom{c-i+1}{k}\) 用于处理多重排列,是将 \(p\) 个 \(k\) 归并到目前的 \(s\) 的方案数
前文的 \(\forall i,t_i\geq\sum\limits_{j=i}^c{s_i}\) 在这里体现为要求 \(j\leq t_i\) 且 \(j-pk\leq t_{i+p}\)
最终的答案为 \(\binom{a+c}{a}\times \sum{(f[1][\_][\_]\times cnt)}\),\(cnt\) 为 \(s\) 序列的不同排列数,根据第二维的 \(\sum s\) 求出
使用前缀和优化 \(\rm dp\) 转移,再加上枚举 \(a,c\) 的复杂度,时间复杂度 \(O(n^5\log n)\),可以通过
#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=240\),\(\rm rk6\)
总结
-
T1 切的有点慢,以后得加快速度
-
T2 最后时间才想出正解,需要加强思维。部分分要大胆写,这次正解就是在部分分里想出来的。写代码要注意细节与范围限制,不能写的太随便
-
T3 其实可以进一步思考,不能满足暴力
-
T4 随缘