考研复试之dp重温

来自http://cppblog.com/menjitianya/archive/2015/10/23/212084.html

和https://www.cnblogs.com/AndyJee/p/4465696.html

感谢作者!

 
1️⃣、动态规划的经典模型
 1、线性模型
       线性模型的是动态规划中最常用的模型,上文讲到的最长单调子序列就是经典的线性模型,这里的线性指的是状态的排布是呈线性的。【例题6】是一个经典的面试题,我们将它作为线性模型的敲门砖。
      
【例题6】在一个夜黑风高的晚上,有n(n <= 50)个小朋友在桥的这边,现在他们需要过桥,但是由于桥很窄,每次只允许不大于两人通过,他们只有一个手电筒,所以每次过桥的两个人需要把手电筒带回来,i号小朋友过桥的时间为T[i],两个人过桥的总时间为二者中时间长者。问所有小朋友过桥的总时间最短是多少。
图二-1-1
      
每次过桥的时候最多两个人,如果桥这边还有人,那么还得回来一个人(送手电筒),
也就是说N个人过桥的次数为2*N-3(倒推,当桥这边只剩两个人时只需要一次,三个人的情况为来回一次后加上两个人的情况...)。
有一个人需要来回跑,将手电筒送回来(也许不是同一个人,realy?!)
这个回来的时间是没办法省去的,并且回来的次数也是确定的,为N-2,如果是我,我会选择让跑的最快的人来干这件事情,但是我错了...
如果总是跑得最快的人跑回来的话,那么他在每次别人过桥的时候一定得跟过去,于是就变成就是很简单的问题了,
花费的总时间: 
      T = 
minPTime * (N-2) + (totalSum-minPTime)
      
来看一组数据 四个人过桥花费的时间分别为 1 2 5 10,按照上面的公式答案是19,但是实际答案应该是17。
      
具体步骤是这样的:
      
第一步:1和2过去,花费时间2,然后1回来(花费时间1);
      
第二歩:3和4过去,花费时间10,然后2回来(花费时间2);
      第三部:1和2过去,花费时间2,总耗时17。
      
所以之前的贪心想法是不对的。
      
我们先将所有人按花费时间递增进行排序,
假设前i个人过河花费的最少时间为opt[i],
那么考虑前i-1个人过河的情况,即河这边还有1个人,河那边有i-1个人,并且这时候手电筒肯定在对岸,所以
      
opt[i] = opt[i-1] + a[1] + a[i]        (让花费时间最少的人把手电筒送过来,然后和第i个人一起过河)
      
如果河这边还有两个人,一个是第i号,另外一个无所谓,河那边有i-2个人,并且手电筒肯定在对岸,所以
      opt[i] = opt[i-2] + a[1] + a[i] + 2*a[2]    (让花费时间最少的人把电筒送过来,然后第i个人和另外一个人一起过河,由于花费时间最少的人在这边,所以下一次送手电筒过来的一定是花费次少的,送过来后花费最少的和花费次少的一起过河,解决问题)
      
所以 opt[i] = min{opt[i-1] + a[1] + a[i] , opt[i-2] + a[1] + a[i] + 2*a[2] }
 

2.区间模型
 区间模型的状态表示一般为d[i][j],表示区间[i, j]上的最优解,然后通过状态转移计算出[i+1, j]或者[i, j+1]上的最优解,逐步扩大区间的范围,最终求得[1, len]的最优解。
     
【例题7】给定一个长度为n(n <= 1000)的字符串A,求插入最少多少个字符使得它变成一个回文串。
      典型的区间模型,回文串拥有很明显的子结构特征,即当字符串X是一个回文串时,在X两边各添加一个字符'a'后,aXa仍然是一个回文串,我们用d[i][j]来表示A[i...j]这个子串变成回文串所需要添加的最少的字符数,那么对于A[i] == A[j]的情况,很明显有 d[i][j] = d[i+1][j-1] (这里需要明确一点,当i+1 > j-1时也是有意义的,它代表的是空串,空串也是一个回文串,所以这种情况下d[i+1][j-1] = 0);当A[i] != A[j]时,我们将它变成更小的子问题求解,我们有两种决策:
      1、在A[j]后面添加一个字符A[i];
      2、在A[i]前面添加一个字符A[j];
      根据两种决策列出状态转移方程为:
            d[i][j] = min{ d[i+1][j], d[i][j-1] } + 1;                (每次状态转移,区间长度增加1)
      空间复杂度O(n^2),时间复杂度O(n^2)
3.背包模型
背包问题是动态规划中一个最典型的问题之一。由于网上有非常详尽的背包讲解
,这里只将常用部分抽出来,具体推导过程详见
 
       
a.0/1背包
            
有N种物品(每种物品1件)和一个容量为V的背包。放入第 i 种物品耗费的空间是Ci,得到
的价值是Wi。求解将哪些物品装入背包可使价值总和最大。
               
f[i][v]表示前i种物品恰好放入一个容量为v的背包可以获得的最大价值。
            
决策为第i个物品在前i-1个物品放置完毕后,是选择放还是不放,状态转移方程为: 
               
f[i][v] = max{ f[i-1][v], f[i-1][v - Ci] +Wi }
               
时间复杂度O(VN),空间复杂度O(VN) 
 
 b.完全背包
               
有N种物品(每种物品无限件)和一个容量为V的背包。放入第 i 种物品耗费的空间是Ci,得到
的价值是Wi。求解将哪些物品装入背包可使价值总和最大。
               
f[i][v]表示前i种物品恰好放入一个容量为v的背包可以获得的最大价值。
               f[i][v] = max{ f[i-1][v - kCi] + kWi  | 0 <= k <= v/Ci
 }        (当k的取值为0,1时,这就是01背包的状态转移方程)
               时间复杂度O( VNsum{V/Ci} ),空间复杂度在用滚动数组优化后可以达到
O( V )。
               进行优化后(此处省略500字),状态转移方程变成:
               f[i][v] = max{ f[i-1][v],  f[i][v - Ci] +Wi }   
               时间复杂度降为
O(VN)。
      c.多重背包
               有N种物品(每种物品Mi件)和一个容量为V的背包。放入第i种物品耗费的空间是Ci,得到
的价值是Wi。求解将哪些物品装入背包可使价值总和最大。
               f[i][v]表示前i种物品恰好放入一个容量为v的背包可以获得的最大价值。
               f[i][v] = max{ f[i-1][v - kCi] + kWi  | 0 <= k <= Mi }
               时间复杂度O( Vsum(Mi) ),
空间复杂度仍然可以用滚动数组优化后可以达到
O( V )。
               优化:采用二进制拆分物品,将Mi个物品拆分成容量为1、2、4、8、... 2^k、Mi-( 2^(k+1) - 1 ) 个对应价值为Wi、2Wi、4Wi、8Wi、...、2^kWi、(
Mi-( 2^(k+1) - 1 )
)Wi的物品,然后采用01背包求解。
               这样做的时间复杂度降为O(Vsum(logMi) )。
【例题8】一群强盗想要抢劫银行,总共N(N <= 100)个银行,第i个银行的资金为Bi亿,抢劫该银行被抓概率Pi,问在被抓概率小于p的情况下能够抢劫的最大资金是多少?
      p表示的是强盗在抢银行时至少有一次被抓概率的上限,那么选择一些银行,并且计算抢劫这些银行都不被抓的的概率pc,则需要满足1 - pc < p。这里的pc是所有选出来的银行的抢劫时不被抓概率(即1 - Pi)的乘积,于是我们用资金作为背包物品的容量,概率作为背包物品的价值,求01背包。状态转移方程为:
      f[j] = max{ f[j], f[j - pack[i].B] * (1-pack[i].p) }
      最后得到的f[i]表示的是抢劫到 i 亿资金的最大不被抓概率。令所有银行资金总和为V,那么从V-0进行枚举,第一个满足1 - f[i] < p的i就是我们所要求的被抓概率小于p的最大资金。
 
4.状态压缩模型
 
5.树状模型

【例题11】给定一颗树,和树上每个结点的权值,求一颗非空子树,使得权和最大。

   
 

      用d[1][i] 表示i这个结点选中的情况下,以i为根的子树的权和最大值;

      用d[0][i]表示i这个结点不选中的情况下,以i为根的子树的权和最大值;

      d[1][i] = v[i] + sum{ d[1][v] | v是i的直接子结点 && d[1][v] > 0 }

      d[0][i] = max( 0, max{ max( d[0][v], d[1][v] ) | v是i的直接子结点 } )

      这样,构造一个以1为根结点的树,然后就可以通过dfs求解了。

      这题题目要求求出的树为非空树,所以当所有权值都为负数的情况下需要特殊处理,选择所有权值中最大的那个作为答案。

最长回文子序列:

要求:

给定字符串,求它的最长回文子序列长度。回文子序列反转字符顺序后仍然与原序列相同。例如字符串abcdfcba中,最长回文子序列长度为7,abcdcba或abcfcba。

思路:

动态规划思想

对于任意字符串,如果头尾字符相同,那么字符串的最长子序列等于去掉首尾的字符串的最长子序列加上首尾;如果首尾字符不同,则最长子序列等于去掉头的字符串的最长子序列和去掉尾的字符串的最长子序列的较大者。

因此动态规划的状态转移方程为:

设字符串为str,长度为n,p[i][j]表示第i到第j个字符间的子序列的个数(i<=j),则:

状态初始条件:dp[i][i]=1 (i=0:n-1)

状态转移方程:dp[i][j]=dp[i+1][j-1] + 2  if(str[i]==str[j])

                   dp[i][j]=max(dp[i+1][j],dp[i][j-1])  if (str[i]!=str[j])

代码:

以下代码中的两层循环变量i,j的顺序可以改变,但必须满足i<=j的条件。

计算dp[i][j]时需要计算dp[i+1][*]或dp[*][j-1],因此i应该从大到小,即递减;j应该从小到大,即递增。

代码:

int func11(string str){

         int n = str.length();

         vector<vector<int> >dp(n,vector<int>(n));

        

         for(int i=n-1;i>=0;i--){

                  dp[i][i] = 1;

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

                          if(str[i]==str[j])

                                   dp[i][j] = dp[i+1][j-1]+2;

                          else

                                   dp[i][j] = max(dp[i+1][j],dp[i][j-1]);

                                  

                  }

         }

         return dp[0][n-1];

}

 

###回文子序列的个数:

要求:

给定字符串,求它的回文子序列个数。回文子序列反转字符顺序后仍然与原序列相同。例如字符串aba中,回文子序列为"a", "a", "aa", "b", "aba",共5个。内容相同位置不同的子序列算不同的子序列。

思路:

动态规划思想

对于任意字符串,如果头尾字符不相等,则字符串的回文子序列个数就等于去掉头的字符串的回文子序列个数+去掉尾的字符串的回文子序列个数-去掉头尾的字符串的回文子序列个数;如果头尾字符相等,那么除了上述的子序列个数之外,还要加上首尾相等时新增的子序列个数,1+去掉头尾的字符串的回文子序列个数,1指的是加上头尾组成的回文子序列,如aa,bb等。

因此动态规划的状态转移方程为:

设字符串为str,长度为n,p[i][j]表示第i到第j个字符间的最长子序列的长度(i<=j),则:

状态初始条件:dp[i][i]=1 (i=0:n-1)

状态转移方程:dp[i][j]=dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]  if(str[i]!=str[j])

                   dp[i][j]=dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]+dp[i+1][j-1]+1=dp[i+1][j] + dp[i][j-1]+1  if (str[i]==str[j])

int func12(string str){

         int n = str.length();

         vector<vector<int> >dp(n,vector<int>(n));

         for(int i=n-1;i>=0;i--){

                  dp[i][i] = 1;

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

                          dp[i][j] = dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1];

                          if(str[i] == str[j])

                                   dp[i][j] += 1+dp[i+1][j-1];

                  }

         }

         return dp[0][n-1];

}

未完待续。。。
 
posted @ 2019-03-03 11:52  顾本无缘  阅读(283)  评论(0编辑  收藏  举报