动态规划——一些杂例

  通过上文关于介绍动态规划的文章,我们知道,作为一种思想,在用动态规划解决问题的时候在不同的情景中有着灵活的变化,动态规划中一些经典的模型,笔者将会专门开文章来讨论,而这篇文章,用来记录一些利用动态规划解决的杂例。

 

  我们直接来看一个问题。(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)
  

Problem Description
在讲述DP算法的时候,一个经典的例子就是数塔问题,它是这样描述的:
有如下所示的数塔,要求从顶层走到底层,若每一步只能走到相邻的结点,则经过的结点的数字之和最大是多少?                         
       9
    12 15
   10 6  8
  2 18 9 5
19 7 10 4 16
 


  为了更好的将问题子问题化,并注意到表示这个二叉树某节点位置需要两个维度的参量——即第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)
 

Problem Description
据说在很久很久以前,可怜的兔子经历了人生中最大的打击——赛跑输给乌龟后,心中郁闷,发誓要报仇雪恨,于是躲进了杭州下沙某农业园卧薪尝胆潜心修炼,终于练成了绝技,能够毫不休息得以恒定的速度(VR m/s)一直跑。兔子一直想找机会好好得教训一下乌龟,以雪前耻。 最近正值HDU举办50周年校庆,社会各大名流齐聚下沙,兔子也趁此机会向乌龟发起挑战。虽然乌龟深知获胜希望不大,不过迫于舆论压力,只能接受挑战。 比赛是设在一条笔直的道路上,长度为L米,规则很简单,谁先到达终点谁就算获胜。 无奈乌龟自从上次获胜以后,成了名龟,被一些八卦杂志称为“动物界的刘翔”,广告不断,手头也有了不少积蓄。为了能够再赢兔子,乌龟不惜花下血本买了最先进的武器——“"小飞鸽"牌电动车。这辆车在有电的情况下能够以VT1 m/s的速度“飞驰”,可惜电池容量有限,每次充满电最多只能行驶C米的距离,以后就只能用脚来蹬了,乌龟用脚蹬时的速度为VT2 m/s。更过分的是,乌龟竟然在跑道上修建了很多很多(N个)的供电站,供自己给电动车充电。其中,每次充电需要花费T秒钟的时间。当然,乌龟经过一个充电站的时候可以选择去或不去充电。 比赛马上开始了,兔子和带着充满电的电动车的乌龟并列站在起跑线上。你的任务就是写个程序,判断乌龟用最佳的方案进军时,能不能赢了一直以恒定速度奔跑的兔子。
 
Input
本题目包含多组测试,请处理到文件结束。每个测试包括四行: 第一行是一个整数L代表跑道的总长度 第二行包含三个整数N,C,T,分别表示充电站的个数,电动车冲满电以后能行驶的距离以及每次充电所需要的时间 第三行也是三个整数VR,VT1,VT2,分别表示兔子跑步的速度,乌龟开电动车的速度,乌龟脚蹬电动车的速度 第四行包含了N(N<=100)个整数p1,p2...pn,分别表示各个充电站离跑道起点的距离,其中0<p1<p2<...<pn<L 其中每个数都在32位整型范围之内。
 
Output
当乌龟有可能赢的时候输出一行 “What a pity rabbit!"。否则输出一行"Good job,rabbit!"; 题目数据保证不会出现乌龟和兔子同时到达的情况。


  数理分析:容易看到,对于给的数据,兔子的时间是确定的,那么我们只需要找到乌龟到达终点的最短时间,然后进行比较即可。显然这是一个多过程的决策以达到最优的问题,那么解决这种问题的利器当然是动态规划啦。
  利用动态规划解决多过程的最优决策问题,首先要基于很合适的子问题化。对于乌龟,我们将起点和终点视为和加油站一样的点,那么在乌龟的跑道上,就有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)
 

Problem Description
因为长期钻研算法, 无暇顾及个人问题,BUAA ACM/ICPC 训练小组的帅哥们大部分都是单身。某天,他们在机房商量一个绝妙的计划"一卡通大冒险"。这个计划是由wf最先提出来的,计划的内容是,把自己的联系方式写在校园一卡通的背面,然后故意将自己的卡"遗失"在某处(如水房,TD,食堂,主M。。。。)他们希望能有MM看到他们遗失卡,能主动跟他们联系,这样就有机会请MM吃饭了。他们决定将自己的一卡通夹在基本相同的书里,然后再将书遗失到校园的各个角落。正当大家为这个绝妙的计划叫好时,大家想到一个问题。很明显,如果只有一张一卡通,那么只有一种方法,即,将其夹入一本书中。当有两张一卡通时,就有了两种选择,即,将两张一卡通夹在一本书里,或者分开夹在不同的书里。当有三张一卡通时,他们就有了5种选择,即: {{A},{B},{C}} , {{A,B},{C}}, {{B,C},{A}}, {{A,C},{B}} ,{{A,B,C}} 于是, 这个邪恶计划的组织者wf希望了解,如果ACM训练对里有n位帅哥(即有N张一卡通),那么要把这些一卡通夹到书里有多少种不同的方法。


  数理分析:据说这道题涉及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;
}

 


 
 

posted on 2016-03-29 21:38  在苏州的城边  阅读(472)  评论(0编辑  收藏  举报

导航