郑州集训day1自闭有感
被拉到郑州培训了
考了一上午莫名自闭
帮助慎老师拿到\(rk1\)非常开心
简述一下题目吧
T1.まんふは函数
考原题还行
据说是\(Huffman\)树
在成爷爷的再三讲解下,我终于明白了
我们可以把\(f(i,j)\)理解为合并了\(n-i+1\)个点,形成了\(j\)棵树这个状态到最终状态也就是\(n\)个点\(1\)棵树的最小代价
于是来思考一下这个方程
第一个方程是新增加了一个节点,这个节点独立作为了一棵树,所以没有什么算贡献的必要
第二个方程比较\(nb\)了就是把现在已经形成的\(j\)棵\(Huffman\)树两两合并,之后算贡献,因为是从\(n\)往前合并的,于是合并一次的话花费是后面\(n-i+1\)个数的和,也就是\(b_i\)
于是问题等价于把这\(n\)个元素合并成一棵树的最小代价
这个的话,不知道合并果子您做过没有
T2.穿越广场
【问题描述】
$L $国的仪仗队要穿越首都广场了。
首都广场可以看做是一块 \(N*M\) 的矩形网 格,仪仗队要从左上角的格点\((0,0)\)行进到右下角的格点\((N,M)\),行进过程中只能 向右走或者向下走。
如果把向右走记为\(R\),把向下走记为\(D\),则仪仗队的 行进序列是一个包含 $M \(个\)R$和 \(N\) 个\(D\)的字符串。
这时,$L $国的首长又提出了一个奇葩的要求。他认为仪仗队行走的序列中必 须包含他给出的两个字符串。请你计算一下,满足首长要求的行进序列有多少种 呢?
【输入格式】
从文件 \(square.in\) 中读入数据。 第一行一个整数 \(T\),表示数据组数。 每组数据的第一行是两个整数 \(M\)、\(N\),表示行进序列由 \(M\) 个\(R\)和 \(N\) 个$ D$构成。 每组数据的第二行和第三行是两个不相同的字符串,表示首长要求这两个字 符串是行进序列的子串。
【输出格式】
输出到文件$ square.out$ 中。 一个整数,表示满足要求的行进序列的数量模 $1000000007 $的值。
【样例输入 1】
2
3 2
RRD
DDR
3 2
R
D
【样例输出 1】
1
10
【数据规模与约定】
对于 \(20\%\)的数据,字符串长度\(<=2\)。
对于 \(50\%\) 的数据,\(1<=N,M<=50\),字符串长度\(<=50,T=1\)。
对于 \(100\%\) 的数据,\(1<=N,M<=100\),字符串由\(R\)、\(D\)组成且长度\(<=100\), \(1<=T<=10\)。
这个\(t2\)就不是原题了,但是还是非常套路的
先看一下\(50\)分的做法,我们甚至可以来一个\(O(n^4)\)的做法
非常显然我们可以设\(dp[i][j][k][p]\)表示走到了\((i,j)\)这个格子,在第一个串里匹配到了\(k\)位置,在第二个串里匹配到了\(p\)位置的方案数
转移的话我们需要枚举下一步是走\(D\)还是\(R\),之后更新出新的匹配位置,这个需要我们提前处理好第一个串和第二个串的\(next\)数组,之后每次转移就像\(kmp\)那样匹配就好了
代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#define re register
#define maxn 80
#define LL long long
#define inf 999999999
#define max(a,b) ((a)>(B)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
const int mod=1e9+7;
inline int read()
{
char c=getchar();int x=0;while(c<'0'||c>'9') c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-48,c=getchar();return x;
}
char S[2][maxn];
short nx[2][maxn];
short n,m,len[2],T;
int dp[maxn][maxn][maxn][maxn];
inline void getnx(int o)
{
memset(nx[o],0,sizeof(nx[o]));
nx[o][1]=0;
len[o]=strlen(S[o]+1);
for(re int i=2;i<=len[o];i++)
{
int p=nx[o][i-1];
while(p&&S[o][p+1]!=S[o][i]) p=nx[o][p];
if(S[o][p+1]==S[o][i]) nx[o][i]=p+1;else nx[o][p]=0;
}
}
int main()
{
T=read();
while(T--)
{
m=read(),n=read();
scanf("%s",S[0]+1),scanf("%s",S[1]+1);
getnx(0),getnx(1);
memset(dp,0,sizeof(dp));
dp[0][0][0][0]=1;
for(re int i=0;i<=n;i++)
for(re int j=0;j<=m;j++)
for(re int k=0;k<=len[0];k++)
for(re int p=0;p<=len[1];p++)
{
if(!dp[i][j][k][p]) continue;
int kk=0,pp=0;
if(k==len[0]) kk=k;
if(p==len[1]) pp=p;
if(i!=n)
{
if(!kk)
{
kk=k;
while(kk&&S[0][kk+1]!='D') kk=nx[0][kk];
if(S[0][kk+1]=='D') kk++;
else kk=0;
}
if(!pp)
{
pp=p;
while(pp&&S[1][pp+1]!='D') pp=nx[1][pp];
if(S[1][pp+1]=='D') pp++;
else pp=0;
}
dp[i+1][j][kk][pp]=(dp[i+1][j][kk][pp]+dp[i][j][k][p])%mod;
}
kk=0,pp=0;
if(k==len[0]) kk=k;
if(p==len[1]) pp=p;
if(j!=m)
{
if(!kk)
{
kk=k;
while(kk&&S[0][kk+1]!='R') kk=nx[0][kk];
if(S[0][kk+1]=='R') kk++;
else kk=0;
}
if(!pp)
{
pp=p;
while(pp&&S[1][pp+1]!='R') pp=nx[1][pp];
if(S[1][pp+1]=='R') pp++;
else pp=0;
}
dp[i][j+1][kk][pp]=(dp[i][j+1][kk][pp]+dp[i][j][k][p])%mod;
}
}
printf("%d\n",dp[n][m][len[0]][len[1]]);
}
return 0;
}
发现我们记录两个串的匹配位置真是太奢侈了,我们考虑把这两个串的信息整合一下
发现我们只需要开一个\(AC\)自动机就好了呀
于是设\(dp[i][j][k][0/1/2/3]\)表示到格子\((i,j)\)在自动机上走到了\(k\)位置,匹配的状态是\(0/1/2/3\)这些个二进制数
我们发现这个样子就非常好转移了,每次需要转移的话直接利用\(son[k][R]\)或\(son[k][D]\)同时维护出结束标记就好了
代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#define re register
#define maxn 105
#define LL long long
#define inf 999999999
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
const int mod=1e9+7;
inline int read()
{
char c=getchar();int x=0;while(c<'0'||c>'9') c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-48,c=getchar();return x;
}
char S[2][maxn];
int n,m,T,len[2],cnt;
int son[maxn+maxn][2],flag[maxn+maxn],fail[maxn+maxn];
int dp[maxn][maxn][maxn+maxn][4];
inline void ins(int o)
{
len[o]=strlen(S[o]+1);
int now=0;
for(re int i=1;i<=len[o];i++)
if(S[o][i]=='D') S[o][i]=0;else S[o][i]=1;
for(re int i=1;i<=len[o];i++)
{
if(!son[now][S[o][i]]) son[now][S[o][i]]=++cnt;
now=son[now][S[o][i]];
}
flag[now]|=(1<<(o));
}
inline void Build()
{
std::queue<int> q;
for(re int i=0;i<2;i++) if(son[0][i]) q.push(son[0][i]);
while(!q.empty())
{
int k=q.front();q.pop();
flag[k]|=flag[fail[k]];
for(re int i=0;i<2;i++)
if(son[k][i]) fail[son[k][i]]=son[fail[k]][i],q.push(son[k][i]);
else son[k][i]=son[fail[k]][i];
}
}
int main()
{
freopen("square.in","r",stdin);
freopen("square.out","w",stdout);
T=read();
while(T--)
{
m=read(),n=read();
scanf("%s",S[0]+1),scanf("%s",S[1]+1);
memset(dp,0,sizeof(dp)),memset(son,0,sizeof(son)),memset(flag,0,sizeof(flag)),memset(fail,0,sizeof(fail));
cnt=0,ins(0),ins(1),Build();
dp[0][0][0][0]=1;
for(re int i=0;i<=n;i++)
for(re int j=0;j<=m;j++)
for(re int k=0;k<=cnt;k++)
for(re int p=0;p<4;p++)
if(dp[i][j][k][p])
{
if(i!=n)
{
int kk=son[k][0];
dp[i+1][j][kk][p|flag[kk]]=(dp[i+1][j][kk][p|flag[kk]]+dp[i][j][k][p])%mod;
}
if(j!=m)
{
int kk=son[k][1];
dp[i][j+1][kk][p|flag[kk]]=(dp[i][j+1][kk][p|flag[kk]]+dp[i][j][k][p])%mod;
}
}
int ans=0;
for(re int i=0;i<=cnt;i++) ans=(ans+dp[n][m][i][3])%mod;
printf("%d\n",ans);
}
return 0;
}
T3.存印器
【问题描述】
一个存印器是包含 \(M\) 个变量并且可以接受两种指令的机器,这两种指令分 别为:
-
\(variable=integer\)
-
\(print(variable)\)
\(variable\) 可以用 \(M\) 个变量中的任意一个变量的名称替换,变量名称用一个小 写字母表示。\(integer\) 可以用任意整数替换。
$print $打印出变量中当前存储的值。 存印器执行一次变量赋值操作需要耗费的代价为 \(integer\) 转化为二进制数后 包含 $1 $的个数。执行打印操作不耗费代价。
现在有一个长度为$ N$ 的整数序列需要打印。如果用存印器按顺序打印这个序 列,至少需要多少代价呢?
【输入格式】
从文件$ saveprint.in$ 中读入数据。 第一行两个整数 \(N,M\)。 第二行 \(N\) 个整数,表示需要打印的序列。
【输出格式】
输出到文件$ saveprint.out$ 中。 输出一个整数表示最小代价。
【样例输入 1】
7 2
1 2 2 4 2 1 2
【样例输出 1】
4
【数据规模与约定】
对于 \(20\%\)的数据,\(1≤n≤10\)。
对于 \(50\%\)的数据,\(1≤m≤2\)。
对于 \(100\%\)的数据,\(1≤n≤250\), \(1≤m≤26\),序列中的整数在\(1-10^9\)范围 内。
写了一个\(50\)的\(m=2\)的\(dp\)拿了\(60\)非常开心
\(dp\)太傻了就不说了
这题正解一看就是网络流啊,而且一看就是费用流
发现这个其实和某一道最小权路径覆盖一模一样啊
我们把每个点\(i\)拆成\(i\)和\(i'\)两个点,之后搞一个超级源点\(S\)向每一个\(i\)连一条容量为\(1\)费用为\(0\)的边,\(i'\)向\(T\)连容量为\(1\)费用为\(0\)的边
之后每个点\(i\)向\((i+1)',(i+2)'...n'\)连容量为\(1\)费用为\(bit\)的边,\(bit\)为指向的点的二进制中\(1\)的个数,如果这条边连接的是两个权值相同的点那么这条边的费用为\(0\),这样就可以表示存印器里权值的切换
之后\(S\)向\(S'\)连容量为\(m\)的边,\(S'\)向所有的\(i'\)连边,容量为\(1\),费用为对应的\(bit\),表示存印器刚开始被存入了这个权值
代码就不写了,就是一个费用流的板子了