【dp专题】在经历了时空扭曲后的总结

在很久很久很久以后,我终于又回到了coder的舞台。
那么我们来水一发dp吧!
好懒啊不想动

字符染色

【原题】BZOJ 2958

题目描述
给出一个长度为N由B、W、X三种字符组成的字符串S,你需要把每一个X染成B或W中的一个。
这里写图片描述

输入格式 2237.in
第一行两个整数N和K。
第二行一个长度为N的字符串。

输出格式 2237.out
一个整数表示答案。

输入样例 2237.in
5 2
XXXXX
输出样例 2237.out
4
数据规模:
对于20%的数据, N <= 20
对于50%的数据, N <= 2000
对于100%的数据, 1 <= N, K <= 1000000

【思路】
一看这题,显然后面的方案数是可以从前面以某种玄学的方式推过来的,那么考虑dp。

由于条件的限制性,我们若是一位一位作dp,是无法处理有连续一段长度为k这个东西的。这样一来,我们就要考虑多开一维记录是否有连续一段B/W长度为k,不难发现,需要记录的实际上只有3种情况——BW都不满足,B满足,BW都满足。

好的,接下来分析如何判断是否能满足连续一段都为B/W。
其实挺容易想到的,就是利用前缀和记录前面一共有几个B/W(用hw和hb记录吧),例如我们要判断B,那么我们只需要判断hw[i]与hw[i-k]是不是相等即可。

接下来就是转移了啊~~~
用f[i][j][k]表示到了第i位,状态是j(即0是没有连续段长为k的,1是B有W没 有,2是BW都有),k是这一位选了什么(0是B,1是W)
保证连续的B后面一定有一个W,连续的W后有一个B,答案就是f[n+1][2][0]

假设这一位选B有

f[i][j][0]=f[i-1][j][1]+f[i-1][j][0]
if(i-k+1到i没有W)
    f[i][1][0]=f[i][1][0]+f[i-k][0][1];

然而这样会有重复,由于f[i][0][0]是乱转移,中间有可能已经有B了,所以:

if(i-k+1到i没有W)f[i][0][0]=f[i][0][0]-f[i-k][0][1];

这样就可以了??是吧。。。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int mod=1e9+7;
const int MAXN=1e6+10;
int f[MAXN][3][3];//the ith,the state,this one choice
                    //state 0=none,1=have k B,2=have k W
int hb[MAXN],hw[MAXN];
int n,k;
char st[MAXN];

/*void debug()
{
    for(int i=1;i<=n;++i)
    {
        for(int j=0;j<=2;++j)
        {
            printf("%d ",f[i][j][0]);
        }
        printf("\n");
    }
    printf("\n");
    for(int i=1;i<=n;++i)
    {
        for(int j=0;j<=2;++j)
        {
            printf("%d ",f[i][j][1]);
        }
        printf("\n");
    }
    printf("\n");
}*/

void init()
{
    scanf("%d%d",&n,&k);
    scanf("%s",st+1);
    st[++n]='X';

    for(int i=1;i<=n;++i)
    {
        hb[i]=hb[i-1];hw[i]=hw[i-1];
        if(st[i]=='B')
            hb[i]++;
        if(st[i]=='W')
            hw[i]++;
    }
}

void solve()
{
    f[0][0][1]=1;
    for(int i=1;i<=n;++i)
    {
        if(st[i]=='B')
            for(int j=0;j<3;++j)
                f[i][j][0]=(f[i-1][j][0]+f[i-1][j][1])%mod;
        else
        if(st[i]=='W')
            for(int j=0;j<3;++j)
                f[i][j][1]=(f[i-1][j][0]+f[i-1][j][1])%mod;
        else
            for(int j=0;j<3;++j)
            {
                f[i][j][0]=(f[i-1][j][0]+f[i-1][j][1])%mod;
                f[i][j][1]=(f[i-1][j][0]+f[i-1][j][1])%mod;
            }

        if(i<k)
            continue;

        if(st[i]=='B' || st[i]=='X')
        {
            if(hw[i]==hw[i-k])
            {
                f[i][1][0]=f[i][1][0]+f[i-k][0][1];
                f[i][1][0]%=mod;
                f[i][0][0]=f[i][0][0]-f[i-k][0][1]+mod;
                f[i][0][0]%=mod;
            }
        }
        if(st[i]=='W' || st[i]=='X')
        {
            if(hb[i]==hb[i-k])
            {
                f[i][2][1]=f[i][2][1]+f[i-k][1][0];
                f[i][2][1]%=mod;
                f[i][1][1]=f[i][1][1]-f[i-k][1][0]+mod;
                f[i][1][1]%=mod;
            }
        }
    }
//  debug();
    printf("%d\n",f[n][2][0]);
}

int main()
{
    freopen("2237.in","r",stdin);
    freopen("2237.out","w",stdout);

    init();
    solve();

    return 0;
}

============我是分割线============

ABA字符串

【原题】BZOJ 3620

题目描述
给出一个长度为n的字符串(均为小写字母),以及一个整数k。
问该字符串的所有子串中,能被切分为A+B+A的形式且 len(A) >= k, len(B) >= 1 的子串数量有多少。

输入格式 2240.in
第一行一个字符串
第二行一个整数k

输出格式 2240.out
一个整数表示答案。

输入样例 2240.in
abcabcabc
2
输出样例 2240.out
8
数据规模:
对于30%的数据: 1 <= n <= 100
对于100%的数据: 1 <= n <= 10000, 1 <= k <= 100

n<=10000 显然是直接给你暴力水的- -

看到字符串匹配,立马就有什么AC自动机之类的东西出来。
于是我们思路清晰,用KMP来暴力

枚举子串的左端点,然后枚举右端点
对于每个子串S我们要判定是否存在一个长度在[k,(|S|-1)>>1]之间的前缀与后缀匹配
那我们就求出长度不超过(|S|-1)>>1的最长前后缀,判断是否>=k即可

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;

const int MAXN=10010;
int nex[MAXN];
char s[MAXN];
int k,n,ans;

int main()
{
    freopen("2240.in","r",stdin);
    freopen("2240.out","w",stdout);

    scanf("%s",&s);
    scanf("%d",&k);
    n=strlen(s);

    ans=0;
    for (int i=1;i<=n-k;i++)
    {
        nex[0]=nex[1]=0;
        int x=0;
//      printf("0 0 ");
        for (int j=2; j<=n-i+1; j++)
        {
            while(x && s[i-1+x]!=s[i+j-2]) 
                x=nex[x];
            if (s[i+x-1]==s[i+j-2]) 
                x++;
            nex[j]=x;
//          printf("%d ",nex[j]);
        }
//      printf("\n");
        int tmp=ans;
        x=0;
        for (int j=2;j<=n-i+1;j++)
        {
            while(x && s[i-1+x]!=s[i+j-2]) 
                x=nex[x];
            if (s[i-1+x]==s[i+j-2]) 
                x++;
            while((x<<1|1)>j) 
                x=nex[x];
            if (x>=k) 
                ans++;
        }
/*      printf("!!!%d\n",ans-tmp);
        for(int j=0;j<=n;++j)
            printf("%d ",nex[j]);
        printf("\n");*/
    }

    printf("%d\n",ans);
    return 0;
}

==========凑热闹的分割线===========

The Intriguing Obsession

【原题】codeforces 869C

【题目大意】
有三个集合,分别含有a、b、c个点,要求给这些点连线,也可以全都不连,要求连接后,每两点距离为1,在同一集合的两点最短距离至少为3,问多少种不同的连接方案。

【思路】
首先我们简化问题,思考集合两两之间的关系。我们发现,若每两个集合都保证满足条件,那最后结果一定满足条件。

那么问题就变得比较简单了,两个集合间若要最短距离至少为3,那每个集合中的点只能同时与另一个集合中的一个点相连。

那么不难得出,若两个集合间需要连k条线,则可以在集合A中选k个点,在集合B中选k个点,共有k!种连接方式,即

C[A][k]*C[B][k]*k!。

两个集合间最少可连0条,最多可连min(A,B),因此k的范围为0~min(A,B)。
这是组合数的思考方式。

然后我们还可以引申一下,用个dp来搞搞这个东西
我们发现每个点其实就是选和不选的区别,所以

dp[i][j]=dp[i-1][j]+dp[i-1][j-1]*j;

就这样。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

typedef long long LL;
const int mod=998244353;
const int MAXN=5005;
int a,b,c;
LL ans,dp[MAXN][MAXN];

void init()
{
    dp[0][0]=1;
    for(int i=0;i<=5000;++i)
        dp[0][i]=dp[i][0]=1;

    for(int i=1;i<=5000;++i)
        for(int j=1;j<=5000;++j)
            dp[i][j]=(dp[i-1][j]+dp[i-1][j-1]*1ll*j)%mod;
}

void solve()
{
    scanf("%d%d%d",&a,&b,&c);
    ans=(dp[a][b]*dp[b][c]%mod)*dp[c][a]%mod;
    printf("%I64d\n",ans);
}

int main()
{
//  freopen("C.in","r",stdin);
//  freopen("C.out","w",stdout);

    init();
    solve();

    return 0;
}

说实话也并不是很想打其他了啊www
那就到这里结束吧~~

撒花~~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值