动态规划算法介绍

动态规划算法及其应用

动态规划是运筹学的一个分支,是求解决策过程最优化的数学方法。动态规划算法的思想和分治算法类似,基本思想也是将待求问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。因此,动态规划算法是自底向上进行计算的,每一个子问题可以看成是一个状态,关键是找到状态转移方程,这样才能从子问题一步步推得原问题的解。

具体的动态规划算法是多种多样的,不同的问题所设计出的动态规划算法都不同。但设计动态规划算法的步骤确是相同的,使用动态规划的基本设计思想设计算法解决最优化问题通常包含以下几个步骤:

  1. 找出最优解的性质,并刻画结构特征。
  2. 递归定义最优值。
  3. 以自底向上的方式计算最优值。
  4. 根据计算最优值时得到的信息,构造最优解。

 

应用一:矩阵连乘问题:

对于一个任意的矩阵相乘的链,我们该如何确定矩阵的相乘顺序,即如何加括号,使得矩阵链连乘的数乘次最少?

求解过程:

假设有我们需要计算一个矩阵链,矩阵链为动态规划算法介绍 ,对于计算这么一个问题,先要它的子问题是什么,对于这么一个矩阵链连乘的问题,必然存在着一个性质:我们肯定会将矩阵链分割为两个部分,先计算左边的部分的矩阵连乘的结果,再计算右边的部分的矩阵连乘的结果,然后再将左边的部分和右边的部分相乘。那么如何描述将矩阵链分为左右两个部分,我们引入断点的概念,若矩阵链动态规划算法介绍Ak动态规划算法介绍 处断开,那么原问题就变成了动态规划算法介绍 ,继而再计算左右两部分分别如何加括号。

所以根据上面的分析,最优解的子问题结构的特征为计算A[i,j]的最优次序所包含的计算矩阵子链A[i:k]和A[k+1:j]的次序也是最优的。

建立递归关系:设计算A[i:j],动态规划算法介绍 ,所需要的最少数乘次数为m[i,j],原问题的最优值为m[1,n]。

动态规划算法介绍 ,动态规划算法介绍 ,因此,动态规划算法介绍

动态规划算法介绍 时,动态规划算法介绍 ,这里动态规划算法介绍 的维数为动态规划算法介绍

 

因此我们我们可以编写以下程序:

矩阵链连乘的测试用例为A1(30*35),A2(35*15),A3(15*5),A4(5*10),A5(10*20),A6(20*25),使用MatrixChainOrder()函数确定矩阵链连乘的最小数乘次数,再将记录记录最小数乘次数的数组m进行打印,以及打印记录最佳断点的数据s,最后使用traceback()函数确定连乘顺序。

最后通过打印的测试结果正确。

 

#include<iostream>

#include<iomanip>

using namespace std;

void MatrixChainOrder(int s[7][7],int m[7][7],int p[],int n){

     int i,j,k,t,r;

     for(i=1;i<=n;i++){

          m[i][i] = 0;

     }

     for(r=1;r<=n-1;r++){

          for(i=1;i<=n-r;i++){

               j = i + r;

               m[i][j] = m[i][i] + m[i+1][j] + p[i-1] * p[i] * p[j];

               s[i][j] = i;

               for(k=i+1;k<j;k++){

                    t = m[i][k] + m[k+1][j] + p[i-1] * p[k] * p[j];

                    if(t<m[i][j]){

                         m[i][j] = t;

                         s[i][j] = k;

                    }

               }

          }

     }

}

void traceback(int s[7][7],int i,int j){

     if(i==j) return;

     traceback(s,i,s[i][j]);

     traceback(s,s[i][j]+1,j);

     cout << "Multiply A" << i << "," << s[i][j] << " and A" << s[i][j]+1 << "," << j << endl;

}

int main(){

     int n = 6;

     int p[7] = {30,35,15,5,10,20,25};

     int s[7][7];

     int m[7][7];

     int i,j;

     MatrixChainOrder(s,m,p,n);

     for(i=1;i<=6;i++){

          for(j=1;j<=6;j++){

               if(j<=i) cout << "      ";

               else cout << setw(5)  << m[i][j] << " ";

          }

          cout << endl;

     }

     for(i=1;i<=6;i++){

          for(j=1;j<=6;j++){

               if(j<=i) cout << "      ";

               else cout << setw(5)  << s[i][j] << " ";

          }

          cout << endl;

    }

    traceback(s,1,6);

    return 0;

}

 

 

结果为下:

动态规划算法介绍

 

 

应用二:分糖果问题:

There are N children standing in a line. Each child is assigned a rating value.

You are giving candies to these children subjected to the following requirements:

  • Each child must have at least one candy.
  • Children with a higher rating get more candies than their neighbors.

What is the minimum candies you must give?

 

求解过程:

题目的约束条件:每个孩子至少一个糖果,若孩子的value比邻接孩子的value高,那么这个孩子的糖果数一定比邻接孩子的糖果数目多。

对于n个孩子的原问题,可以转化为先解决n-1个孩子的分糖果问题,在这基础上,在队尾加上一个孩子,然后再对糖果分配进行一定修改。因此前1个孩子为状态1,前2个孩子为状态2,前3个孩子为状态3,以此类推,可以由状态i推得状态i+1,最终得到n个孩子的分糖果问题。

因为每个孩子至少有一个糖果,因此我们先对每个孩子分配一个糖果。然后先考虑前2个孩子,前2个孩子,前3个孩子,以此类推。

考虑前i个孩子的时候,如果第i个孩子的value比第i-1个孩子的value值高,那么第i个孩子的糖果比第i-1个孩子的糖果多一个。

如果第i个孩子的value比第i-1个孩子的value值低,那么第i个孩子的糖果为1。但考虑到第i-1个孩子的糖果也可能只有1个,这么做会出错。因此,若第i-1个孩子的value值比第i个孩子高,那么修改第i-1个孩子的糖果比第i个孩子多一个。再判断第i-2个孩子的value是否比第i-1个孩子的value高,若是,则第i-2个孩子的糖果值比第i-1个孩子的糖果值多一个。接着考虑第i-3,i-4,i-5,……个孩子,直到第j-1个孩子的value小于等于第j个孩子为止。

根据以上思路,我们可以编写得到程序:

      

 

class Solution {

public:

    int candy(vector<int> &ratings) {

         int len = ratings.size();

         int i;

         int j;

         int total = 0;

         int flag = 0;

         int min;

         int max;

         vector<int> num(len, 1);

         for (i = 1; i <= len - 1; i++) {

             if (ratings[i] > ratings[i - 1]) {

                  num[i] = num[i - 1] + 1;

             }

             else {

                  if (num[i - 1] >= 2) num[i] = 1;

                  else {

                      num[i] = 1;

                      for (j = i - 1; j >= 0; j--) {

                          if (ratings[j] > ratings[j + 1] && num[j] <= num[j + 1]) num[j] = num[j] + 1;

                          if (ratings[j] <= ratings[j + 1]) break;

                      }

                  }

             }

         }

         for (i = 0; i <= len - 1; i++) {

             total = total + num[i];

         }

         return total;

    }

};

 

 

 

 

 

 

应用三:字符串拆分问题

Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words.

For example, given
s ="leetcode",
dict =["leet", "code"].

Return true because"leetcode"can be segmented as"leet code".

求解过程:

本题的解决思路为从字符串左边开始扫描,由于是从左边开始扫描,因此,在扫描的过程中,每一次扫描都可以看成一个状态。

当扫描的位置达到字符串s的第i个位置时,我们就判断字符串的0到i的字符串是否能够进行拆分。此次拆分成功的问题可能分解为上次拆分成功的问题加上上次拆分成功后新的起始位置到现在位置的字符串是否能匹配到dict。例如题目中的例子,leetcode能够成功拆分,必然时先leet成功进行拆分,然后code也能进行成功拆分。

我们使用一个数组a[]记录字符串s的扫描位置到达i时,子字符串0-i是否能够进行拆分,若能,则a[i]=true,若不能, a[i]=false。

根据上述分析,我们可以编写程序如下:

 

class Solution {

public:

    bool wordBreak(string s, unordered_set<string> &dict) {

         int i = 1;

         int j;

         int len = s.length();

         bool a[len + 1];

         for (int q = 0; q <= len; q++) a[q] = false;

         a[0] = true;

         for (; i <= len; i++) {

             for (j = 0; j < i; j++) {

                  if (a[j] == true) {

                      if (dict.find(s.substr(j, i - j)) != end(dict)) {

                          a[i] = true;

                      }

                  }

             }

         }

         return a[len];

    }

};