被拉到郑州培训了
考了一上午莫名自闭
帮助慎老师拿到(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
",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
",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),表示存印器刚开始被存入了这个权值
代码就不写了,就是一个费用流的板子了