BZOJ 2958: 序列染色 && BZOJ 3193: [JLOI2013]地形生成 —— 肆虐的DP

前言

从前,我有两篇题解的坑还没有填,一道是DP,另一道也是DP。


BZOJ 2958: 序列染色

Description

  给出一个长度为N由B、W、X三种字符组成的字符串S,你需要把每一个X染成B或W中的一个。
  对于给出的K,问有多少种染色方式使得存在整数a,b,c,d使得:
  1<=a<=b < <script type="math/tex" id="MathJax-Element-1"><</script>c<=d<=N
  Sa,Sa+1,…,Sb均为B
  Sc,Sc+1,…,Sd均为W
  其中b=a+K-1,d=c+K-1
  由于方法可能很多,因此只需要输出最后的答案对109+7取模的结果。


Input

  第一行两个正整数N,K
  第二行一个长度为N的字符串S
 


Output

  一行一个整数表示答案%(10^9+7)。


Sample Input

5 2
XXXXX


Sample Output

4

数据约定

  对于20%的数据,N<=20

  对于50%的数据,N<=2000

  对于100%的数据,1<=N<=10^6,1<=K<=10^6


解题思路(DP+补偿转移)

暴力就不说了,超时or难算,优雅的正解就在下面。

我们考虑记 f[i][j][s] 代表当前第 i 位,状态为j,0代表没有连续K个B或W,1代表有连续K个B,2代表既有连续K个B,又有连续K个W,最后的一个字符记为s,0代表B,1代表W(X就都可以)。

很明显,如果当前这位不是W的话

f[i][j][0]=f[i1][j][0]+f[i1][j][1]

不是B也一样。

这里是没有那么多的XJB转移,在这里不符合状态的那部分的方案也暂且记住,比如j=0的状态在乱转K位后包含了1,1包含了2等等。

然后如果当前不是W且连续K个都没有W的话,我们可以转移

f[i][1][0]=f[i][1][0]+f[iK][0][1]

然后将前面乱转移多的那部分减去 (连续K个累计的一齐减去)
f[i][0][0]=f[i][0][0]f[iK][0][1]

这里可以称作 补偿转移,或者就是 容斥原理

连续一段都是W的转移也一样,将 f[i][1][1] 多余部分减去。

炒鸡棒的DP,先乱转移一段,“欲擒故纵”,最后在特定的时候一齐减去多余的。

一开始就让第0个位置放W,最后的答案就是 f[n][2][0]+f[n][2][1]

时间复杂度: O(n)

做这种题要么算出重复的减掉,要么直接算不重复的。往往前者要用容斥原理,后者要用巧妙的DP。

嫉妒使我补偿转移。


代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
#include <cmath>
#define Mod 1000000007
#define N 1000010

using namespace std;

int n, K, sumB[N], sumW[N], f[N][3][2];
char s[N];

int main(){

    scanf("%d%d", &n, &K);

    scanf("%s", &s);

    f[0][0][1] = 1;
    for(int i = 1; i <= n; i++){
        sumB[i] = sumB[i-1] + (s[i-1] == 'B');
        sumW[i] = sumW[i-1] + (s[i-1] == 'W');
        if(s[i-1] != 'W')  for(int j = 0; j < 3; j++)  f[i][j][0] = (f[i-1][j][0] + f[i-1][j][1]) % Mod;
        if(s[i-1] != 'B')  for(int j = 0; j < 3; j++)  f[i][j][1] = (f[i-1][j][0] + f[i-1][j][1]) % Mod;

        if(i < K)  continue;
        if(s[i-1] != 'W' && sumW[i] == sumW[i-K]){  
            f[i][1][0] = (f[i][1][0] + f[i-K][0][1]) % Mod;
            f[i][0][0] = (f[i][0][0] - f[i-K][0][1] + Mod) % Mod;
        }
        if(s[i-1] != 'B' && sumB[i] == sumB[i-K]){  
            f[i][2][1] = (f[i][2][1] + f[i-K][1][0]) % Mod;
            f[i][1][1] = (f[i][1][1] - f[i-K][1][0] + Mod) % Mod;
        }
    }

    printf("%d\n", (f[n][2][0] + f[n][2][1]) % Mod);
    return 0;
}

BZOJ 3193: [JLOI2013]地形生成

Description

最近IK正在做关于地形建模的工作。其中一个工作阶段就是把一些山排列成一行。每座山都有各不相同的标号和高度。为了遵从一些设计上的要求,每座山都设置了一个关键数字,要求对于每座山,比它高且排列在它前面的其它山的数目必须少于它的关键数字。
显然满足要求的排列会有很多个。对于每一个可能的排列,IK生成一个对应的标号序列和等高线序列。标号序列就是按顺序写下每座山的标号。等高线序列就是按顺序写下它们的高度。例如有两座山,这两座山的一个合法排列的第一座山的标号和高度为1和3,而第二座山的标号和高度分别为2和4,那么这个排列的标号序列就是1 2,而等高线序列就是3 4.
现在问题就是,给出所有山的信息,IK希望知道一共有多少种不同的符合条件的标号序列和等高线序列。


Input

输入第一行给出山的个数N。接下来N行每行有两个整数,按照标号从1到N的顺序分别给出一座山的高度和关键数。


Output

输出两个用空格分隔开的数,第一个数是不同的标号序列的个数,第二个数是不同的等高线序列的个数。这两个答案都应该对2011取模,即输出两个答案除以2011取余数的结果


Sample Input

2
1 2
2 2


Sample Output

2 2


HINT

对于所有的数据,有1<=N<=1000,所有的数字都是不大于10^9的正整数。


解题思路(DP+组合数学)

这道题我考试时是不会做的,过了好长时间了,又有点忘了。

考虑第一问:将山按高度从大到小排序插入,因为有相等的高度,所以排在第i的数可以插入的位置有 min(i,b[i]+1)+ji 个,用乘法原理合并。

第二问就比较麻烦了,里面有个组合数学的DP,还有个前缀和优化(好像只优化了空间)。

照样把高度相同的一起考虑,高度相同按关键值升序排。然后根据乘法原理乘起来。在就是把x个一样的球放入y个不同位置的方案,就是不管顺序,取组合数。记 dp[i][j] 为前 i 个球放入前j个位置的方案。第 i 个放在j或不放:

dp[i][j]=dp[i1][j]+dp[i][j1]

然后发现可以滚动,变成 f[j]+=f[j1](1<=j<=b[i])

注意这里不插入的方案是有1个的,用 f[0]=1 去代表它,并使它加入转移(这里有点懵B)。

当前高度下的答案是 f[j] , 注意边界的处理,放的位置不能进入同样高度区间里(前面算出装不下有剩余的已经包含此状态),也不能达到最后一个的关键值。

时间复杂度 O(nlogn+n2)

DP要消除顺序的限制,一般要排序,强制规定顺序,使其能够正确转移,与排列组合有关的DP的固定套路要掌握,比如枚举取走的与剩下的状态是一一对应的(简单数学都不会就完了)。


代码

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <iostream>
#define N 1010
#define M 2011
using namespace std;

int n, ans1 = 1, ans2 = 1, f[N];
struct Data{
    int h, num;
    bool operator < (const Data& Q)const {return h > Q.h || (h == Q.h && num < Q.num);}
}mt[N];

int main(){

    scanf("%d", &n);
    for(int i = 1; i <= n; i++)  scanf("%d%d", &mt[i].h, &mt[i].num), mt[i].num --;

    sort(mt+1, mt+n+1);

    for(int i = 1, t = 1; i <= n; i = ++t){
        while(t < n && mt[t].h == mt[t+1].h)  ++ t;
        memset(f, 0, sizeof(f));
        f[0] = 1;
        for(int j = i; j <= t; j++){
            ans1 = (ans1 * (min(i, mt[j].num+1) + j - i)) % M;
            for(int k = 1; k <= min(i-1, mt[j].num); k++)  f[k] = (f[k] + f[k-1]) % M; 
        }
        int temp = 0;
        for(int j = 0; j <= min(i-1, mt[t].num); j++)  temp = (temp + f[j]) % M;
        ans2 = (ans2 * temp) % M;
    }

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

总结

一入DP深似海,从此智商是路人。


这里写图片描述

我已和魔女签订契约,是不能成为神的朋友的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值