动态规划——一些杂例
通过上文关于介绍动态规划的文章,我们知道,作为一种思想,在用动态规划解决问题的时候在不同的情景中有着灵活的变化,动态规划中一些经典的模型,笔者将会专门开文章来讨论,而这篇文章,用来记录一些利用动态规划解决的杂例。
我们直接来看一个问题。(Problem source : Light OJ 1047)
Description
The people of Mohammadpur have decided to paint each of their houses red, green, or blue. They've also decided that no two neighboring houses will be painted the same color. The neighbors of house i are houses i-1 and i+1. The first and last houses are not neighbors.
You will be given the information of houses. Each house will contain three integers "R G B" (quotes for clarity only), where R, G and B are the costs of painting the corresponding house red, green, and blue, respectively. Return the minimal total cost required to perform the work.
Input
Input starts with an integer T (≤ 100), denoting the number of test cases.
Each case begins with a blank line and an integer n (1 ≤ n ≤ 20) denoting the number of houses. Each of the next n lines will contain 3 integers "R G B". These integers will lie in the range [1, 1000].
Output
For each case of input you have to print the case number and the minimal cost.
题目大意:给出整数n,表示有n户人家(连成一条直线),并给出二维数组cost[n][3],用来记录每户人家将房子涂成R、G、B三种颜色的费用,现在需要你找到一张方案,使得相邻的两家的房子颜色不重复,并且n户人家涂色的费用最小。
数理分析:我们设置二维数字dp[i][j],表示表示第i户人家涂成某种颜色时的最小费用(j = 0、1、2分别代表R、G、B)。假设我么你现在在涂第i个房子,会有三种情况。基于题设的限制条件,我们不难找到状态转移方程。
第i个房子是R:dp[i][0] = cost[n][0] + dp[i-1][1] + dp[i-1][2]
第i个房子是G:dp[i][1] = cost[n][1] + dp[i-1][0] + dp[i-1][2]
第i个房子是B:dp[i][2] = cost[n][2] + dp[i-1][0] + dp[i-1][1]
不难看出,通过这个方程我们便很好的完成了对整个决策过程所有状态的记录,因此选出最小方案也变得十分容易。
参考代码如下。
#include<stdio.h> #include<algorithm> using namespace std; const int maxn = 1005; int cost[maxn][3]; int dp[maxn][3]; int Min(int a , int b , int c) { if(a <= b && a<= c) return a; if(b <= a && b<= c) return b; if(c <= a && c<= b) return c; } int main() { int n; int T; scanf("%d",&T); for(int j = 1;j <= T;j++) { scanf("%d",&n); for(int i = 0;i < n;i++) scanf("%d%d%d",&cost[i][0],&cost[i][1],&cost[i][2]); dp[0][0] = cost[0][0]; dp[0][1] = cost[0][1]; dp[0][2] = cost[0][2]; for(int i = 1;i < n;i++) { dp[i][0] = cost[i][0] + min(dp[i-1][1] , dp[i-1][2]); dp[i][1] = cost[i][1] + min(dp[i-1][2] , dp[i-1][0]); dp[i][2] = cost[i][2] + min(dp[i-1][0] , dp[i-1][1]); } int temp; temp = Min(dp[n-1][0] , dp[n-1][1] , dp[n-1][2]); printf("Case %d: %d\n",j,temp); } }
下面我们来看一个动态规划当中最为基础和经典的一个问题——数塔问题。(Problem source : hdu 2084)
有如下所示的数塔,要求从顶层走到底层,若每一步只能走到相邻的结点,则经过的结点的数字之和最大是多少?
为了更好的将问题子问题化,并注意到表示这个二叉树某节点位置需要两个维度的参量——即第i层的第j个元素。因此我们设置二维数组a[i][j]来表示第i层(自上往下)第j个元素(自左往右)记录的权值。
那么我们现在想要知道的是,从最底层到达(1,1)的一条权值最大的路径。我们容易将整个问题子问题化,我们设置dp[i][j]记录从最底层某点到达(i,j)的最大权值和,那么考察我们想要得到的dp[1][1]和与之相邻的两个子节点dp[2][1],dp[2][2]的关系,显然dp[1][1] = max(dp[2][1] , dp[2][2]) + a[1][1],因此对dp[1][1]的求解就转化成了对dp[2][1],dp[2][2]的求解。同样对它们的求解也会进行类似上述过程的转化,最终整个问题会细分到最后一层,也就是到达递归的初始条件,然后回溯回来,便可得到dp[1][1]。
这种递归式的算法理论上行得通,但是计算速度太慢,基于递归,我们都是可以将其写成递推的。上述过程是自上而下,而后又回溯到顶层,我们不妨直接从底层开始。
考察(i,j)和它相邻的两个子节点(i+1,j),(i+1,j+1)如下的状态转移方程。
dp[i][j] = max(dp[i+1][j] , dp[i+1][j+1]) + a[i][j]
参考代码如下。
#include<stdio.h> #include<string.h> #include<algorithm> using namespace std; const int maxn = 105; int main() { int a[maxn][maxn]; int dp[maxn][maxn]; int t; scanf("%d",&t); while(t--) { int n; memset(dp , 0 , sizeof(dp)); scanf("%d",&n); for(int i = 1;i <= n;++i) { for(int j = 1;j <= i;++j) scanf("%d",&a[i][j]); } for(int j = 1;j <= n;++j) dp[n][j] = a[n][j]; for(int i = n - 1;i >= 1;--i) for(int j = 1;j <= i;++j) dp[i][j] = max(dp[i+1][j] , dp[i+1][j+1]) + a[i][j]; printf("%d\n",dp[1][1]); } }
我们再来看一道问题。(Problem source : hdu 2059)
数理分析:容易看到,对于给的数据,兔子的时间是确定的,那么我们只需要找到乌龟到达终点的最短时间,然后进行比较即可。显然这是一个多过程的决策以达到最优的问题,那么解决这种问题的利器当然是动态规划啦。
利用动态规划解决多过程的最优决策问题,首先要基于很合适的子问题化。对于乌龟,我们将起点和终点视为和加油站一样的点,那么在乌龟的跑道上,就有N + 2个点。我们讨论到达第如何求得N+2个点的最短时间,容易看到,如果我们知道起点到第i个点的最短时间的一个序列{time[1],time[2],time[3]……time[N]},那么在此基础上我们考虑在第i个加油站加完油之后直接开到终点所需要时间,得到一个新的序列{time'[1],time'[2],……time'[N]},很显然,这包含了所有的决策情况,所以time[N+1] = min( { time[i] | i ∈ [1,N]})。
这种子问题化的分析顺着自然的逻辑给出了各个决策之间的递推关系,而在我们实际计算中,显然我们要从前面开始。即设置dp[i]表示起点到第i个加油站的最短时间,我们模拟如下的动态规划过程。
for i 1 to N + 1
for j 0 to i
dp[i] = min({dp[j] + time(j to i)})
其中time(j to i)表示乌龟在第j个点充完电后直接去第i个点所需的时间。
基于这种动态过程的分析,参考代码如下。
#include<iostream> #include<cstdio> using namespace std; const double INF = 1000000000.0; int main() { int L , C , T; int VR , VT1 , VT2; int p[105] , N; double dp[105]; double temp; while(cin>>L) { cin>>N>>C>>T; cin>>VR>>VT1>>VT2; p[0] = 0; for(int i = 1;i <= N;i++) cin>>p[i]; p[N+1] = L; dp[0] = 0; for(int i = 1;i <= N + 1;i++) { double Min = INF; for(int j = 0;j < i;j++) { int l = p[i] - p[j]; if(l < C) temp = l*1.0/VT1; else temp = C*1.0/VT1 + (l-C)*1.0/VT2; if(j != 0) temp += T; if(dp[j] + temp < Min) Min = dp[j] + temp; } dp[i] = Min; } if(dp[N+1] > (L*1.0/VR)) printf("Good job,rabbit!\n"); else printf("What a pity rabbit!\n"); } }
我们再来看一道利用dp求解的杂例。(Problem source : hdu 2512)
数理分析:据说这道题涉及string数和bell数,笔者会在《组合数学》一栏中给出详细分析,这里我们单纯得从dp的角度来看。
子问题化:对于这个问题我们应该如何子问题呢?关键在于找到表征不同状态的参数,容易看到,集合的元素i是一维参量,同时某状态分成的子集和的组数j也是一维参数,即我们用dp[i][j]来表示有i张卡,分成j组的不同组合数。
状态转移方程:我们模拟求解dp[i][j]的过程,寻求其与前面子问题的联系。对于加入的第j个元素,有且仅有如下两种情况。
①第j个元素单独一组,则容易看到有dp[i-1][j-1]种情况。
②第j个元素没有单独一组,则容易看到有dp[i-1][j]*j种情况。
综合起来,我们得到状态转移方程如下。
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]*j。
简单的参考代码如下。
#include<cstdio> #include<iostream> using namespace std; const int maxn = 2012; int dp[maxn][maxn]; int answer[maxn]; int main() { int t , n ; dp[1][1] = 1; answer[1] = 1; for(int i = 2;i < maxn;i++) { dp[i][i] = dp[i][1] = 1; answer[i] = 2;//dp[i][i] = dp[i][1] = 1. for(int j = 2;j < i;j++) { dp[i][j] = (dp[i-1][j]*j + dp[i-1][j-1])%1000; answer[i] += dp[i][j]; } answer[i] %= 1000; } scanf("%d",&t); while(t--) { scanf("%d",&n); printf("%d\n",answer[n]); } return 0; }