zoukankan      html  css  js  c++  java
  • dp套dp学习笔记

    1 dp 和 dp套dp

    • dp 套 dp 中的 dp 一定是 dp 套 dp 的基础,而 dp 套 dp 也就是从 dp 的基础上 dp 而来的。
    • 没错,上面这句话就是套娃。

    为了方便大家理解,从这句话开始,dp套dp 将作为一个不加空格的词,方便区分。

    dp 的时候,我们一般会设计一个 dp 状态,然后让 dp 从某个状态向某个状态转移,最终统计某些状态最终的值。

    而在 dp套dp 里面,我们就将内层 dp 的结果作为外层 dp 的状态进行 dp。

    2 正片

    举个例子:

    • (1)

    • 求长度为 (n),字符集为 ( exttt{N}, exttt{O}, exttt{I}),与给定字符串 (S)( ext{LCS})(len) 的字符串数量。
    • (|S|leq15)(nleq10^3)

    2.1 ( ext{LCS}) 回顾

    显然对于字符串 (A)(B),我们记 ( ext{LCS}_{x,y})(A)(x) 位,(B)(y) 位的最长公共子序列长度,则

    [ ext{LCS}_{x,y}=left{ egin{aligned} ext{LCS}_{x-1,y-1}+1&&A_x=B_y\ max{ ext{LCS}_{x-1,y}, ext{LCS}_{x,y-1}}&&A_x eq B_y\ end{aligned} ight.]

    2.2 转化

    第一部分最后一句中,我们提到外层 dp 的状态就是内层 dp 的结果,于是一个最暴力的想法就是记录所有 ( ext{LCS}) 作为状态。

    • 小补充:dp套dp 中内层的转移应该是一个自动机的状态,但是笔者对自动机理解不深,所以本文仍然用“状态”和“转移”描述自动机的边。

    此时,我们要记录的是长度为 (i),和 (S)( ext{LCS}) 为某个数组的字符串数量。

    然而我们发现,如果我们在某个字符串后面加一个新的字符,只会新生成一行 ( ext{LCS})而这一行 ( ext{LCS}) 完全通过上一行生成!

    (配图,咕了)

    于是我们只要记录 ( ext{LCS}) 最后一行为某个数组的字符串数量了。

    然后我们还发现 ( ext{LCS}_{x+1,y}- ext{LCS}_{x,y}) 只能是 (0) 或者 (1),于是我们还能差分最后一行得到一个 (01) 字符串并再次状压。

    于是我们就能写出很优美的 dp套dp 了。

    算法复杂度 ( ext O(large2^{|S|}m|Sigma|))

    • 示例代码(可以通过该题)。
    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    inline int read()
    {
        int x=0;char ch=getchar();
        while(ch<'0' || ch>'9') ch=getchar();
        while('0'<=ch && ch<='9'){x=x*10+(ch^48);ch=getchar();}
        return x;
    }
    const int p=1000000007;
    int nxt[100003][4],f[2][100003],ans[1003];
    char s[23];
    const char ch[4]={'A','T','C','G'};
    void solve()
    {
    	scanf("%s",s+1);
    	int n=strlen(s+1),m=read(),st=1<<n;
    	for(int i=0,tmp[23],res[23]; i<st; ++i) 
    	{
    		res[0]=tmp[0]=0;
    		for(int j=1,k=i; j<=n; ++j,k>>=1) tmp[j]=tmp[j-1]+(k&1);
    		for(int k=0,num; k<4; ++k)
    		{
    			num=0;
    			for(int j=1; j<=n; ++j) res[j]=(s[j]==ch[k])?tmp[j-1]+1:max(tmp[j],res[j-1]),num+=(1<<(j-1))*(res[j]-res[j-1]);
    			nxt[i][k]=num;
    		}
    	}
    	memset(f,0,sizeof(f));
    	f[0][0]=1;
    	for(int i=0; i<m; ++i)
    	{
    		for(int j=0; j<st; ++j) f[(i&1)^1][j]=0;
    		for(int j=0; j<st; ++j) for(int k=0; k<4; ++k) (f[(i&1)^1][nxt[j][k]]+=f[i&1][j])%=p;
    	}
    	for(int i=0; i<=n; ++i) ans[i]=0;
    	for(int i=0; i<st; ++i) (ans[__builtin_popcount(i)]+=f[m&1][i])%=p;
    	for(int i=0; i<=n; ++i) printf("%lld
    ",ans[i]);
    }
    signed main()
    {
    	for(int T=read(); T--;) solve();
    	return 0;
    }
    

    2.4 小试牛刀

    • (2)

    • 求长度为 (n),字符集为 ( exttt{N}, exttt{O}, exttt{I})不包含子串 ( exttt{NOI}) 的字符串中,与给定字符串 (S)( ext{LCS})(len) 的字符串数量。
    • (|S|leq15)(nleq10^3)

    同步记录是否有后缀是 ( t{N}, t{NO}) 即可。

    • 示例代码(可以通过该题)。
    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    inline int read()
    {
        int x=0;char ch=getchar();
        while(ch<'0' || ch>'9') ch=getchar();
        while('0'<=ch && ch<='9'){x=x*10+(ch^48);ch=getchar();}
        return x;
    }
    const int p=1000000007;
    int nxt[100003][3],f[2][100003][3],ans[1003];
    char s[23];
    const char ch[3]={'N','O','I'};
    signed main()
    {
    	int m=read(),n=read(),st=1<<n;
    	scanf("%s",s+1);
    	for(int i=0,tmp[23],res[23]; i<st; ++i) 
    	{
    		res[0]=tmp[0]=0;
    		for(int j=1,k=i; j<=n; ++j,k>>=1) tmp[j]=tmp[j-1]+(k&1);
    		for(int k=0,num; k<3; ++k)
    		{
    			num=0;
    			for(int j=1; j<=n; ++j) res[j]=(s[j]==ch[k])?tmp[j-1]+1:max(tmp[j],res[j-1]),num+=(1<<(j-1))*(res[j]-res[j-1]);
    			nxt[i][k]=num;
    		}
    	}
    	memset(f,0,sizeof(f));
    	f[0][0][0]=1;
    	for(int i=0; i<m; ++i)
    	{
    		for(int j=0; j<st; ++j) f[(i&1)^1][j][0]=f[(i&1)^1][j][1]=f[(i&1)^1][j][2]=0;
    		for(int j=0; j<st; ++j) 
    		{
    			(f[(i&1)^1][nxt[j][0]][1]+=f[i&1][j][0]+f[i&1][j][1]+f[i&1][j][2])%=p;
    			(f[(i&1)^1][nxt[j][1]][0]+=f[i&1][j][0]+f[i&1][j][2])%=p;
    			(f[(i&1)^1][nxt[j][1]][2]+=f[i&1][j][1])%=p;
    			(f[(i&1)^1][nxt[j][2]][0]+=f[i&1][j][0]+f[i&1][j][1])%=p;
    		}
    	}
    	for(int i=0; i<=n; ++i) ans[i]=0;
    	for(int i=0; i<st; ++i) (ans[__builtin_popcount(i)]+=f[m&1][i][0]+f[m&1][i][1]+f[m&1][i][2])%=p;
    	for(int i=0; i<=n; ++i) printf("%lld
    ",ans[i]);
    	return 0;
    }
    

    3 你已经学会基本操作了,接下来请A掉这道题

    首先,请和我一起虔诚地膜拜 zhouAKngyang,因为他一年前在考场上就爆切了这道题。

    此题中,内层 dp 也比较难想,我们先考虑内层 dp,即给你一些牌,判断有没有和。

    • zhouAKngyang:你先想一会,不要马上看题解。

    可惜以 delta_X 的实力想 114514 年还是不会……

    首先,不难发现我们只需要知道编号为 (x) 的牌的数量,不妨把一些牌的集合记为每种牌的出现次数,例如 ({1,1,1,2,3,4,5,6,7,8,9,9,9}) 记为 ({3,1,1,1,1,1,1,1,3}),第 (i) 张牌的出现次数为 (cnt_i)

    我们记 (dp_{i,0,x,y}) 为考虑编号 (leq i) 的牌,去掉 (x) 个编号为 (i-1)(x+y) 个编号为 (i) 的牌后,最多可以组成的面子数量,记 (dp_{i,1,x,y}) 为在这些牌中选一个雀头后最多可以组成的面子数量。

    我们在从 (dp_{i}) 转移到 (dp_{i+1}) 的时候,不难发现 (i+1) 这种牌的贡献只可能是这几种:做顺子的第一张(对应 (y)),做顺子的第二张(对应 (x)),做顺子的第三张(对应 (dp_i)(x) 转移到 (dp) 值)和做刻子(直接转移到 (dp) 值)。

    如果 (dp_{i,1}) 的某个值已经为 (4),或者 (dp_i) 对应前 (i) 位已经有 (7) 个对子,我们就认定这个状态可以是终止状态了,不再继续转移。

    由于三个相同的顺子等价于三个刻子,我们发现 (x,y<3),因此 (dp_i) 只有 (18) 种可能。而由于某些神奇的原因,这 (18) 种可能只能组成大约 (2000)(dp_i)!于是现在只有 (2000) 个本质不同的 (dp_i) 了,我们就可以用 (f_{st,j,k}) 表示状态为 (st),已经选了前 (j) 种共 (k) 张牌的情况数了。

    时间复杂度 (O(n^2|S|))(S) 代表 (dp_i) 的集合。

  • 相关阅读:
    Python判断列表是否已排序的各种方法及其性能分析
    Python实现C代码统计工具(四)
    Python代码统计工具
    Python实现C代码统计工具(三)
    Python实现C代码统计工具(一)
    Python标准输出重定向
    为C函数自动添加跟踪语句
    Python2.7字符编码详解
    Python实现Linux命令xxd -i功能
    1124 Raffle for Weibo Followers (20 分)
  • 原文地址:https://www.cnblogs.com/dead-X/p/14274671.html
Copyright © 2011-2022 走看看