UVA 10328 , ZOJ 3747 (DP 递推)

来源:互联网 发布:大数据解决方案 编辑:程序博客网 时间:2024/06/03 08:16

DP 递推

有一类需要求出在某一序列中连续出现某一种字母或者数字的总个数问题:比如一枚硬币连续抛出n次求出至少连续出现k次正面的种类个数,等。请注意“至少”这个词语,会有很多情况,所以问题并没有我们想象的简单。

思路:

解决这种问题的思路首先需要转化一下:问题是求出至少出现k次连续,转化为至多出现k次。

列如:

dp[i][j]
表示第i次投掷硬币时,所呈现的序列中最多有j个连续的正面在一起。那么如果问题求出n次投掷,至少有k 个硬币连续的情况,我们就可以输出:
dp[n][n]dp[n][m1]

而:

dp[i][j]=dp[i1][j]2
当然这个式子是不对的,也可以这么认为在某些条件是错的。

所以需要特判:当

i<=jdp[i][j]=dp[i1][j]2
原因是因为i < j 那么第i 次投掷结果是什么都不影响,所以有两种情况。当i = j + 1 时,很明显有中情况是如果前i-1种都是正面的话就不满足状态了,而这种情况只有一种所以
dp[i][j]
减一就行。当i > j + 1时,有一种不满足题意的情况是紧挨着第i次之前有j次是连续的正面并且第i-j-1次投掷一定是反面所以对于出现这样的情况的次数就是
dp[ij2][j]
,减去它既可。

  • 注意点:首先是转化问题为“至少”才能递推

  • 要考虑到“第i个人”,而不是把注意力集中在j上边

  • 其次是注意边界问题,下面是投掷硬币问题的代码

    UVA 10328

import java.math.*;import java.util.Scanner;public class Main {    /**     * @param args     */    public static void main(String[] args) {        // TODO Auto-generated method stub        Scanner cin = new Scanner(System.in);        BigInteger[][] dp = new BigInteger[110][110];        for(int i = 0;i <= 100; i++) {            dp[0][i] = new BigInteger("1");            dp[1][i] = new BigInteger("2");            dp[i][0] = new BigInteger("1");        }        dp[1][0] = new BigInteger("1");        for(int i = 2;i <= 100; i++) {            for(int j = 1;j <= 100; j++) {                dp[i][j] = dp[i-1][j].multiply(BigInteger.valueOf(2));                if(i == j + 1)                    dp[i][j] = dp[i][j].add(BigInteger.valueOf(-1));                else if(i > j + 1)                     dp[i][j] = dp[i][j].subtract((dp[i-j-2][j]));            }        }        while(cin.hasNext()) {            int n = cin.nextInt();            int m = cin.nextInt();            System.out.println(dp[n][n].subtract((dp[n][m-1])));        }    }}

下面是一道变形题:

有三个军团,现在要从三个军团里面挑选n个人排成一列,但是有两个条件

  • 从军团一种挑选的人至少有m个人排在一起,如果挑选的人大于m个人多余的可以和m个人排在一起,也可以排在其它地方

  • 从军团二中挑选的人至多有k个人排在一起,如果挑选的人大于k,可以排在其它地方,但是不能出现大于k个人排在一起的情况。

    第三个军团无任何要求。

    按照上面这道题的思路:先转化问题为“至多”,那么问题可以输出solve(n,k) -solve(m-1,k) 。

我们定义

dp[i][j]
表示第i个人是j军团的人满足条件的个数(j取0、1、2).

那么对于第一军团的人:

long long sum = (dp[i-1][0] + dp[i-1][1] + dp[i-1][2])%mod;dp[i][2] = sum;  //对于第i个人是军团三的人就没影响if(i <= u)         //这种情况不会出现大于u的连续排列  dp[i][0] = sum;else if(i == u + 1)  dp[i][0] = sum - 1;//这种情况会出现一种全都是第一军团的人,所以减一else dp[i][0] = (sum - dp[i-u-1][1] - dp[i-u-1][2])%mod; /* 这种情况是在紧挨着第i次挑选人之前出现了u个第一军团的人,因为第i-u-1这个人只能是第二军团和第三军团的人,所以把这种情况减去。这里和上道题的i-j-2不同的是硬币在第i-j-1是反面已经确定了,而这里是有两种军团的情况(这里需要多多注意,不同的题需要不同的状态意义)*/

完整代码:

ZOJ 3747

#include <iostream>#include <cstdio>#include <cstring>using namespace std;const int maxn = 1100000;const int mod = 1000000007;long long n,m,k;long long dp[maxn][4];long long solve(long long u,long long v){    dp[0][0] = 1;    dp[0][1] = 0;    dp[0][2] = 0;    for(int i = 1;i <= n; i++) {        long long sum = (dp[i-1][0] + dp[i-1][1] + dp[i-1][2])%mod;        dp[i][2] = sum;        if(i <= u)            dp[i][0] = sum;        else if(i == u + 1)            dp[i][0] = sum - 1;        else dp[i][0] = (sum - dp[i-u-1][1] - dp[i-u-1][2])%mod;        if(i <= v)            dp[i][1] = sum;        else if(i == v + 1)            dp[i][1] = sum - 1;        else dp[i][1] = (sum - dp[i-v-1][0] - dp[i-v-1][2])%mod;    }    return (dp[n][0] + dp[n][1] + dp[n][2])%mod;}int main(){    //freopen("in.txt","r",stdin);    while(scanf("%lld%lld%lld",&n,&m,&k) != EOF) {        printf("%lld\n",((solve(n,k)-solve(m-1,k))%mod+mod)%mod);    }    return 0;}