动态规划的计算方法主要有三种:
- 递归计算(有大量重复计算)
- 递推计算(需要注意边界处理和状态转移方程)
- 记忆化搜索
递推的重点在于
- 找到对于状态的参数描述
- 找到状态转移方程,而寻找状态转移方程的重点在于分类
- 找到边界条件
UVa580 危险的组合
有一些装有铀(用 U 表示)和铅(用 L 表示)的盒子,数量均足够多。要求把 n ( n ≤ 30 )个盒子放成一行,但至少有 3 个 U 放在一起,有多少种放法?例如, n=4, 5, 30 时答案分别为 3,8 和 974791728 。
–《算法竞赛入门经典(第二版)》
样例:
Sample Input
4
5
0
Sample Output
3
8
- 设答案为 f(n)
- 找到状态转移方程
根据“最左边三个 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)=2n−f(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} {2n−3g(i−2)∗2n−i−2 , i=1 , 2⩽i⩽n−1
式中, 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)=2n−3+i=2∑n−1g(i−2)∗2n−i−2
具体的代码可以用递推,也可以用记忆化搜索,但是递推相较而言在本题中更加适用。 - 边界条件
- 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)=2n−f(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
- 设 n 人赛马时最终名次的可能性的个数为 f(n)
- 假设并列第一的有 i 个人,那么除了第一名之外的 n-i 个人争夺第二名,第三名……,n-i 个人(从第二名开始)可能性个数为 f(n-i),那么并列第一有 i 个人的可能性个数为
C
n
i
f
(
n
−
i
)
C_n^if(n-i)
Cnif(n−i)
。 - 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=1∑nCnif(n−i)
- 题目问的是 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=1∑n(Cnimodm)(f(n−i)modm))modm - 但是
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=Cn−1i+Cn−1i−1,通俗来讲,就是:如果我们要计算 n 个物体里面选择 i 个物体的组合,首先从 n 个物体里面任意选择一个物体,比如第 n 个(也可以由杨辉三角直接看出来),可以将组合分为两类:
- 有第 n 个物体的组合,接下来需要从前 n-1 个物体中选择 i-1 个,即 C n − 1 i − 1 C_{n-1}^{i-1} Cn−1i−1
- 没有第 n 个物体的组合,接下来需要从前 n-1 个物体中选择 i-1 个,即 C n − 1 i C_{n-1}^i Cn−1i
所以 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=Cn−1imodm+Cn−1i−1modm
-
C n i = n − i + 1 k C n i − 1 C_n^i=\frac {n-i+1} k C_n^{i-1} Cni=kn−i+1Cni−1,通俗来讲,就是:如果我们要计算 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} Cni−1 一定是整数,但是 n − i + 1 k \frac {n-i+1} k kn−i+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 都是整数。所以不能用这个递推式。
-
- 边界条件,对于组合数来说,边界条件是
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
-
设 d(i,j,k)表示高为 1, 2, 3,…, i 的杆子各一根排成一行,从左边能看到 j 根,从右边能看到 k 根的可能性。
-
按照从大到小的顺序安排各个杆子,假设已经安排完高度为 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(i−1,j−1,k)+d(i−1,j,k−1)+d(i−1,j,k)
-
这里需要考虑数据类型,防止运算溢出。至少需要考虑 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;
}