dp套dp学习笔记

1 dp 和 dp套dp#

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

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

我们发现这不就是 DFA 吗,每个点有出边,有起始状态和终止状态集合,所以我们可以直接记录在若干步之后在 DFA 上的 x 号节点的方案数。

不知道 DFA 也没啥关系,在 dp套dp 里面,我们就将内层 dp 的结果作为外层 dp 的状态进行 dp。

2 正片#

举个例子:

1#

  • 求长度为 n,字符集为 N,O,I,与给定字符串 SLCSlen 的字符串数量。
  • |S|15n103

2.1 LCS 回顾#

显然对于字符串 AB,我们记 LCSx,yAx 位,By 位的最长公共子序列长度,则

LCSx,y={LCSx1,y1+1Ax=Bymax{LCSx1,y,LCSx,y1}AxBy

2.2 转化#

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

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

此时,我们要记录的是长度为 i,和 SLCS 为某个数组的字符串数量。

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

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

然后我们还发现 LCSx+1,yLCSx,y 只能是 0 或者 1,于是我们还能差分最后一行得到一个 01 字符串并再次状压。

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

算法复杂度 O(2|S|m|Σ|)

  • 示例代码(可以通过该题)。
#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\n",ans[i]);
}
signed main()
{
    for(int T=read(); T--;) solve();
    return 0;
}

2.4 小试牛刀#

2#

  • 求长度为 n,字符集为 N,O,I不包含子串 NOI 的字符串中,与给定字符串 SLCSlen 的字符串数量。
  • |S|15n103

同步记录是否有后缀是 N,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\n",ans[i]);
    return 0;
}

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

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

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

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

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

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

时间复杂度 O(n2|S|)S 代表 dpi 的集合。

4 你已经完全掌握 dp套dp 了,接下来请 A 掉这道题#

首先考虑 40 分做法,即 li=ri

我们称第一种操作为竖线,第二种操作为横线。

首先我们发现一个长度 6 的横线可以拆成两条横线,因此横线的长度 5

然后我们发现两条完全一致的横线可以转成一条竖线,因此所有横线都是互不相同的。

所以考虑一个 dp:fx,S 代表放完所有左端点 x 的线,右端点为接下来三个位置的线段数分别为 S1,S2,S3,S4,且 x 的每个堆都合法时至少需要放入的石子数。

不难发现 Si<3,因为如果一个端点连出了三条横线,较长的两条可以长度减一,然后加一条竖线。

算算时间复杂度是 O(nk) 的,其中 k 代表状态数乘以转移数,只有 34×6

到这里都是比较平凡的。

然后出题人大手一挥,考虑 dp套dp。

也就是说我们考虑 gx,S 代表决定完所有左端点 x 的值,fx 数组为 S 的方案数。

直接压整个 fx 显然是不行的,我们套路地使用 bfs 算出可达的状态数,发现是 105 级的。

最后还有一个问题:ri109 怎么办?

不难发现 14 以上的数等价于 14,因为一个点最多只能被 12 个区间覆盖到。

算算时间复杂度是 O(nTNM) 的,其中 N=105M=14,肯定过不去。

dp套dp 可以考虑剪枝,我们可以通过一些人类智慧发现,一个点最多被 3 条线覆盖到!

这个东西是怎么想出来的呢?首先你发现了一个点被一车线覆盖的时候大概率是存在更优方案的,然后你在 dp套dp 的时候枚举一下线段最大值。因为线段数量变少了,数的等价类也变少了,我们发现 45 已经等价,此时只剩约 1.5×104 个状态和 5 个转移。

然后我们发现出题人洛谷把我们卡常了,只能感恩(如果在场上你就过了)。

再进一步地,我们发现左端点 x,右端点 x+2 的线段只会同时存在不超过 2 条。

现在只有 1.2×104 个状态了,加上一些极限卡常就过了。

posted @   dXqwq  阅读(6546)  评论(1编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示
主题色彩