算法之递推


动态规划的计算方法主要有三种:

  1. 递归计算(有大量重复计算)
  2. 递推计算(需要注意边界处理和状态转移方程)
  3. 记忆化搜索

递推的重点在于

  1. 找到对于状态的参数描述
  2. 找到状态转移方程,而寻找状态转移方程的重点在于分类
  3. 找到边界条件

UVa580 危险的组合

有一些装有铀(用 U 表示)和铅(用 L 表示)的盒子,数量均足够多。要求把 n ( n ≤ 30 )个盒子放成一行,但至少有 3 个 U 放在一起,有多少种放法?例如, n=4, 5, 30 时答案分别为 3,8 和 974791728 。
–《算法竞赛入门经典(第二版)》

样例:

Sample Input
4
5
0
Sample Output
3
8

  1. 设答案为 f(n)
  2. 找到状态转移方程
    根据“最左边三个 U ”的位置分类,假定“最左边三个 U ”的位置在 i,i+1,i+2 这里,如果那么右边任意的组合都可以,而左边不能有三个 U 的情况。(而且 i-1 的位置只能为 L ,因为如果为 U ,则最左边的三个 U 应该为 i-1,i,i+1 )。而 n 个盒子"没有 3 个 U 放在一起”的方案数为 g ( n ) = 2 n − f ( n ) g(n)=2^n-f(n) g(n)=2nf(n) , n 个盒子的任意组合的方案数为 2 n 2^n 2n,所以方案数为:
    { 2 n − 3  ,  i = 1 g ( i − 2 ) ∗ 2 n − i − 2  ,  2 ⩽ i ⩽ n − 1 \begin{cases} 2^{n-3} & \text{ , } i=1 \\ g(i-2)*2^{n-i-2} & \text{ , } 2 \leqslant i \leqslant {n-1} \end{cases} {2n3g(i2)2ni2 , i=1 , 2in1
    式中, i=1 时为 UUU***** 这样的情况, $2 \leqslant i \leqslant {n-1} $ 为 ###LUUU***** 其中#的个数为 i-2 ,因为 L 的位置为 i-1 ,*的个数为 n-i-2 。所以:
    f ( n ) = 2 n − 3 + ∑ i = 2 n − 1 g ( i − 2 ) ∗ 2 n − i − 2 f(n)=2^{n-3}+\sum_{i=2}^{n-1} g(i-2)*2^{n-i-2} f(n)=2n3+i=2n1g(i2)2ni2
    具体的代码可以用递推,也可以用记忆化搜索,但是递推相较而言在本题中更加适用。
  3. 边界条件
  • f(0)=f(1)=f(2)=0 ,因为小于 3 的时候不可能有三个 U 放在一起
  • g(0)=1 , g(1)=2 , g(2)=4 ,利用的是 g ( n ) = 2 n − f ( n ) g(n)=2^n-f(n) g(n)=2nf(n) 计算的。

完整代码在文章末尾。

UVa12034 比赛名次

A 、 B 两人赛马,最终名次有 3 种可能:并列第一; A 第一 B 第二; B 第一 A 第二。输入 n ( 1 ≤ n ≤ 1000 ),求 n 人赛马时最终名次的可能性的个数除以 10056 的余数。
–《算法竞赛入门经典(第二版)》

样例:

Sample Input
3
1
2
3
Sample Output
Case 1: 1
Case 2: 3
Case 3: 13

  1. 设 n 人赛马时最终名次的可能性的个数为 f(n)
  2. 假设并列第一的有 i 个人,那么除了第一名之外的 n-i 个人争夺第二名,第三名……,n-i 个人(从第二名开始)可能性个数为 f(n-i),那么并列第一有 i 个人的可能性个数为 C n i f ( n − i ) C_n^if(n-i) Cnif(ni)
  3. f ( n ) = ∑ i = 1 n C n i f ( n − i ) f(n)=\sum _{i=1}^n C_n^i f(n-i) f(n)=i=1nCnif(ni)
  4. 题目问的是 f(n)%10056 ,如果先 f(n),再算余数那么一定会溢出,因为 1000 的组合数是很大的。由于组合数一定是整数, f(n)一定是整数,根据模运算的规则,有
    f ( n ) m o d    m = ( ∑ i = 1 n ( C n i m o d    m ) ( f ( n − i ) m o d    m ) ) m o d    m f(n) \mod m=(\sum _{i=1}^n (C_n^i\mod m)( f(n-i)\mod m))\mod m f(n)modm=(i=1n(Cnimodm)(f(ni)modm))modm
  5. 但是 C n i C_n^i Cni 组合数也是会溢出的,所以我们要利用取余与组合数递推的特点。组合数主要的递推式有两个:
    • C n i = C n − 1 i + C n − 1 i − 1 C_n^i=C_{n-1}^i+C_{n-1}^{i-1} Cni=Cn1i+Cn1i1,通俗来讲,就是:如果我们要计算 n 个物体里面选择 i 个物体的组合,首先从 n 个物体里面任意选择一个物体,比如第 n 个(也可以由杨辉三角直接看出来),可以将组合分为两类:

      • 有第 n 个物体的组合,接下来需要从前 n-1 个物体中选择 i-1 个,即 C n − 1 i − 1 C_{n-1}^{i-1} Cn1i1
      • 没有第 n 个物体的组合,接下来需要从前 n-1 个物体中选择 i-1 个,即 C n − 1 i C_{n-1}^i Cn1i

      所以 C n i m o d    m = C n − 1 i m o d    m + C n − 1 i − 1 m o d    m C_n^i \mod m=C_{n-1}^i \mod m+C_{n-1}^{i-1} \mod m Cnimodm=Cn1imodm+Cn1i1modm

    • C n i = n − i + 1 k C n i − 1 C_n^i=\frac {n-i+1} k C_n^{i-1} Cni=kni+1Cni1,通俗来讲,就是:如果我们要计算 n 个物体里面选择 i 个物体的组合,首先从 n 个物体里面选择 i-1 个物体,之后再在剩余 n-k+1 个物体中选择一个就有了 i 个物体,但是这样就造成了每个组合有 k 次的重复(如把 1 加入 2 中和把 2 加入 1 中是一样的)。我们知道组合数 C n i C_n^i Cni C n i − 1 C_n^{i-1} Cni1 一定是整数,但是 n − i + 1 k \frac {n-i+1} k kni+1 不一定是整数,而 ( a b ) m o d    c = ( a m o d    c ) ( b m o d    c ) (ab) \mod c=(a \mod c)(b\mod c) (ab)modc=(amodc)(bmodc) 要求 a 和 b 都是整数。所以不能用这个递推式。

  6. 边界条件,对于组合数来说,边界条件是C[i][0]=C[i][i]=1,因为利用递推式计算C[i][i]时会涉及C[i-1][i]。整个问题中边界条件是f(0)=1,因为f(1)=1, f ( 1 ) = C 1 1 f ( 0 ) f(1)=C_1^1f(0) f(1)=C11f(0)

完整代码在文章末尾。

UVa1638 杆子的排列

有高为 1, 2, 3,…, n 的杆子各一根排成一行。从左边能看到 l 根,从右边能看到 r 根,求有多少种可能。例如,下图中的两种情况都满足 l=1 , r=2 ( 1 ≤ l , r ≤ n ≤ 20 )。

–《算法竞赛入门经典(第二版)》

样例:

Sample Input
4
4 1 2
4 1 1
5 2 4
20 2 1
Sample Output
2
0
4
6402373705728000

  1. 设 d(i,j,k)表示高为 1, 2, 3,…, i 的杆子各一根排成一行,从左边能看到 j 根,从右边能看到 k 根的可能性。

  2. 按照从大到小的顺序安排各个杆子,假设已经安排完高度为 2~i 的杆子,之后的一步就是安排高度为 1 的杆子,有以下三种情况:

    • 插在最左边,则左边能看到它,右边看不到它。
    • 插在最右边,则右边能看到它,左边看不到它。
    • (有 i-2 个插入位置):插在中间左右都看不到它。

    d ( i , j , k ) = d ( i − 1 , j − 1 , k ) + d ( i − 1 , j , k − 1 ) + d ( i − 1 , j , k ) d(i,j,k)=d(i-1,j-1,k)+d(i-1,j,k-1)+d(i-1,j,k) d(i,j,k)=d(i1,j1,k)+d(i1,j,k1)+d(i1,j,k)

  3. 这里需要考虑数据类型,防止运算溢出。至少需要考虑 d ( 20 , 1 , 1 ) = 2 × 18 ! = 1.28 × 1 0 16 d(20,1,1)=2\times 18!=1.28\times 10^{16} d(20,1,1)=2×18!=1.28×1016 ,而 int 类型是 4 个字节,也就是最多能表示 2 4 × 8 2^{4\times 8} 24×8个数,而$ 2^{10}=1024$ ,那么 2 4 × 8 = 4 × 1 0 9 2^{4\times 8}=4\times 10^9 24×8=4×109 。 long long 型 8 个字节,最多能表示$ 1.6\times 10^{19} $个数。 20 个杆子的排列有 20 ! = 2.4 × 1 0 18 20 !=2.4\times 10^{18} 20!=2.4×1018,是可以表示的。


完整代码

UVa580 危险的组合

//#define LOCAL
#include <iostream>
#include <stdio.h>
#include <time.h>
#include <algorithm>
#include <cstring>

using namespace std;

int f[31],g[31];// 0~30 共 31 个
int main()
{
#ifdef LOCAL
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif // LOCAL
    f[0]=f[1]=f[2]=0;
    g[0]=1;g[1]=2;g[2]=4;
    for(int n=3; n<=30; n++)
    {
        f[n]=1<<(n-3);
        for(int i=2; i<=n-2; i++)
        {
            f[n]+=g[i-2]*(1<<(n-i-2));
        }
        g[n]=(1<<n)-f[n];
    }
    int n;
    while(cin>>n && n)
    {
        cout<<f[n]<<endl;
    }
    return 0;
}

UVa12034 比赛名次

//#define LOCAL
#include <iostream>
#include <stdio.h>
#include <time.h>
#include <algorithm>
#include <cstring>
//#include <string>
//#include <math.h>
#include <vector>
#include <map>
//#include <bitset>
//#include <sstream>
//#include <map>

using namespace std;

const int maxn=1000;
const int MOD=10056;
int C[maxn+1][maxn+1],f[maxn+1];// 因为还有 0 ,所以需要加 1 。

// 递推出所有组合数
void init()
{
    for(int i=0; i<=maxn; i++)
    {
        C[i][0]=C[i][i]=1;
        for(int j=1; j<i; j++)
        {
            C[i][j]=(C[i-1][j]+C[i-1][j-1])%MOD;// C[i-1][j] 已经对 MOD 取余了
        }
    }
}

int main()
{
#ifdef LOCAL
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif // LOCAL
    init();
    f[0]=1;
    for(int n=1; n<=maxn; n++)
    {
        f[n]=0;
        for(int i=1; i<=n; i++)
        {
            f[n]=(f[n]+C[n][i]*f[n-i])%MOD;
        }
    }

    int T,n;
    cin>>T;
    for(int i=1; i<=T; i++)
    {
        cin>>n;
        printf("Case %d: %d\n",i,f[n]);
    }
    return 0;
}

UVa1638 杆子的排列

//#define LOCAL
#include <iostream>
#include <stdio.h>
#include <time.h>
#include <algorithm>
#include <cstring>

using namespace std;

const int maxn=20;

long long d[maxn+1][maxn+1][maxn+1];


int main()
{
#ifdef LOCAL
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif // LOCAL
    memset(d,0,sizeof(d));
    d[1][1][1]=1;
    for(int i=2;i<=20;i++){
        for(int j=1;j<=i;j++){
            for(int k=1;k<=i;k++){
                d[i][j][k]=d[i-1][j][k]*(i-2);
                if(j>1) d[i][j][k]+=d[i-1][j-1][k];
                if(k>1) d[i][j][k]+=d[i-1][j][k-1];
            }
        }
    }

    int T,n,L,R;
    cin>>T;
    while(T--){
        cin>>n>>L>>R;
        cout<<d[n][L][R]<<endl;
    }

    return 0;
}

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值