动态规划如何去找动态转移方程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划如何去找动态转移方程相关的知识,希望对你有一定的参考价值。

1、最长公共子串
假设两个字符串为str1和str2,它们的长度分别为n和m。d[i][j]表示str1中前i个字符与str2中前j个字符分别组成的两个前缀字符串的最长公共长度。这样就把长度为n的str1和长度为m的str2划分成长度为i和长度为j的子问题进行求解。状态转移方程如下:
dp[0][j] = 0; (0<=j<=m)
dp[i][0] = 0; (0<=i<=n)
dp[i][j] = dp[i-1][j-1] +1; (str1[i] == str2[j])
dp[i][j] = 0; (str1[i] != str2[j])
因为最长公共子串要求必须在原串中是连续的,所以一但某处出现不匹配的情况,此处的值就重置为0。
详细代码请看最长公共子串。
2、最长公共子序列
区分一下,最长公共子序列不同于最长公共子串,序列是保持子序列字符串的下标在str1和str2中的下标顺序是递增的,该字符串在原串中并不一定是连续的。同样的我们可以假设dp[i][j]表示为字符串str1的前i个字符和字符串str2的前j个字符的最长公共子序列的长度。状态转移方程如下:
dp[0][j] = 0; (0<=j<=m)
dp[i][0] = 0; (0<=i<=n)
dp[i][j] = dp[i-1][j-1] +1; (str1[i-1] == str2[j-1])
dp[i][j] = maxdp[i][j-1],dp[i-1][j]; (str1[i-1] != str2[j-1])
详细代码请看最长公共子序列。
3、最长递增子序列(最长递减子序列)
因为两者的思路都是一样的,所以只给出最长递减子序列的状态转移方程。假设有序列a1,a2,...,an,我们求其最长递增子序列长度。按照递推求解的思想,我们用F[i]代表若递增子序列以ai结束时它的最长长度。当 i 较小,我们容易直接得出其值,如 F[1] = 1。那么,如何由已经求得的 F[i]值推得后面的值呢?假设,F[1]到F[x-1]的值都已经确定,注意到,以ax 结尾的递增子序列,除了长度为1的情况,其它情况中,ax都是紧跟在一个由 ai(i < x)组成递增子序列之后。要求以ax结尾的最长递增子序列长度,我们依次比较 ax 与其之前所有的 ai(i < x), 若ai小于 ax,则说明ax可以跟在以ai结尾的递增子序列之后,形成一个新的递 增子序列。又因为以ai结尾的递增子序列最长长度已经求得,那么在这种情况下,由以 ai 结尾的最长递增子序列再加上 ax 得到的新的序列,其长度也可以确定,取所有这些长度的最大值,我们即能得到 F[x]的值。特殊的,当没有ai(i < x)小 于ax, 那么以 ax 结尾的递增子序列最长长度为1。 即F[x] = max1,F[i]+1|ai<ax && i<x。
详细代码请看最长递增子序列。
4、最大子序列和的问题
假设有序列a1,a2,...,an,求子序列的和最大问题,我们用dp[i]表示以ai结尾的子序列的最大和。
dp[1] = a1; (a1>=0 && i == 1)
dp[i] = dp[i-1]+ai; (ai>=0 && i>=2)
dp[i] = 0; (dp[i-1] + ai <=0 && i>=2)
详细代码请看最大子序列的和。
5、数塔问题(动态搜索)
给定一个数组data[n][m]构成一个数塔求从最上面走到最低端经过的路径和最大。可以假设dp[i][j]表示走到第i行第j列位置处的最大值,那么可以推出状态转移方程:
dp[i][j] = maxdp[i-1][j-1],dp[i-1][j] + data[i][j];
View Code
6、(01)背包问题
这是一个经典的动态规划问题,另外在贪心算法里也有背包问题,至于二者的区别在此就不做介绍了。
假设有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是c[i],将哪些物品装入背包可使价值总和最大?
每一种物品都有两种可能即放入背包或者不放入背包。可以用dp[i][j]表示第i件物品放入容量为j的背包所得的最大价值,则状态转移方程可以推出如下:
dp[i][j]=maxdp[i-1][j-v[i]]+c[i],dp[i-1][j];
View Code
可以参照动态规划 - 0-1背包问题的算法优化、动态规划-完全背包问题、动态规划-多重背包问题
7、矩阵连乘(矩阵链问题)-参考《算法导论》
例如矩阵链<A1,A2,A3>,它们的维数分别为10*100,100*5,5*50,那么如果顺序相乘即((A1A2)A3),共需10*100*5 + 10*5*50 = 7500次乘法,如果按照(A1(A2A3))顺序相乘,却需做100*5*50 + 10*100*50 = 75000次乘法。两者之间相差了10倍,所以说矩阵链的相乘顺序也决定了计算量的大小。
我们用利用动态规划的方式(dp[i][j]表示第i个矩阵至第j个矩阵这段的最优解,还有对于两个矩阵A(i,j)*B(j,k)则需要i*j*k次乘法),推出状态转移方程:
dp[i][j] = 0; (i ==j,表示只有一个矩阵,计算次数为0)
dp[i][j] = mindp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j]; (i<j && i<=k<j)
dp[1][n]即为最终求解.
View Code
参考技术A 1、最长公共子串
假设两个字符串为str1和str2,它们的长度分别为n和m。d[i][j]表示str1中前i个字符与str2中前j个字符分别组成的两个前缀字符串的最长公共长度。这样就把长度为n的str1和长度为m的str2划分成长度为i和长度为j的子问题进行求解。状态转移方程如下:
dp[0][j]
=
0;
(0<=j<=m)
dp[i][0]
=
0;
(0<=i<=n)
dp[i][j]
=
dp[i-1][j-1]
+1;
(str1[i]
==
str2[j])
dp[i][j]
=
0;
(str1[i]
!=
str2[j])
因为最长公共子串要求必须在原串中是连续的,所以一但某处出现不匹配的情况,此处的值就重置为0。
详细代码请看最长公共子串。
2、最长公共子序列
区分一下,最长公共子序列不同于最长公共子串,序列是保持子序列字符串的下标在str1和str2中的下标顺序是递增的,该字符串在原串中并不一定是连续的。同样的我们可以假设dp[i][j]表示为字符串str1的前i个字符和字符串str2的前j个字符的最长公共子序列的长度。状态转移方程如下:
dp[0][j]
=
0;
(0<=j<=m)
dp[i][0]
=
0;
(0<=i<=n)
dp[i][j]
=
dp[i-1][j-1]
+1;
(str1[i-1]
==
str2[j-1])
dp[i][j]
=
maxdp[i][j-1],dp[i-1][j];
(str1[i-1]
!=
str2[j-1])
详细代码请看最长公共子序列。
3、最长递增子序列(最长递减子序列)
因为两者的思路都是一样的,所以只给出最长递减子序列的状态转移方程。假设有序列a1,a2,...,an,我们求其最长递增子序列长度。按照递推求解的思想,我们用F[i]代表若递增子序列以ai结束时它的最长长度。当
i
较小,我们容易直接得出其值,如
F[1]
=
1。那么,如何由已经求得的
F[i]值推得后面的值呢?假设,F[1]到F[x-1]的值都已经确定,注意到,以ax
结尾的递增子序列,除了长度为1的情况,其它情况中,ax都是紧跟在一个由
ai(i
<
x)组成递增子序列之后。要求以ax结尾的最长递增子序列长度,我们依次比较
ax
与其之前所有的
ai(i
<
x),
若ai小于
ax,则说明ax可以跟在以ai结尾的递增子序列之后,形成一个新的递
增子序列。又因为以ai结尾的递增子序列最长长度已经求得,那么在这种情况下,由以
ai
结尾的最长递增子序列再加上
ax
得到的新的序列,其长度也可以确定,取所有这些长度的最大值,我们即能得到
F[x]的值。特殊的,当没有ai(i
<
x)小
于ax,
那么以
ax
结尾的递增子序列最长长度为1。
即F[x]
=
max1,F[i]+1|ai<ax
&&
i<x。
详细代码请看最长递增子序列。
4、最大子序列和的问题
假设有序列a1,a2,...,an,求子序列的和最大问题,我们用dp[i]表示以ai结尾的子序列的最大和。
dp[1]
=
a1;
(a1>=0
&&
i
==
1)
dp[i]
=
dp[i-1]+ai;
(ai>=0
&&
i>=2)
dp[i]
=
0;
(dp[i-1]
+
ai
<=0
&&
i>=2)
详细代码请看最大子序列的和。
5、数塔问题(动态搜索)
给定一个数组data[n][m]构成一个数塔求从最上面走到最低端经过的路径和最大。可以假设dp[i][j]表示走到第i行第j列位置处的最大值,那么可以推出状态转移方程:
dp[i][j]
=
maxdp[i-1][j-1],dp[i-1][j]
+
data[i][j];
View
Code
6、(01)背包问题
这是一个经典的动态规划问题,另外在贪心算法里也有背包问题,至于二者的区别在此就不做介绍了。
假设有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是c[i],将哪些物品装入背包可使价值总和最大?
每一种物品都有两种可能即放入背包或者不放入背包。可以用dp[i][j]表示第i件物品放入容量为j的背包所得的最大价值,则状态转移方程可以推出如下:
dp[i][j]=maxdp[i-1][j-v[i]]+c[i],dp[i-1][j];
View
Code
可以参照动态规划
-
0-1背包问题的算法优化、动态规划-完全背包问题、动态规划-多重背包问题
7、矩阵连乘(矩阵链问题)-参考《算法导论》
例如矩阵链<A1,A2,A3>,它们的维数分别为10*100,100*5,5*50,那么如果顺序相乘即((A1A2)A3),共需10*100*5
+
10*5*50
=
7500次乘法,如果按照(A1(A2A3))顺序相乘,却需做100*5*50
+
10*100*50
=
75000次乘法。两者之间相差了10倍,所以说矩阵链的相乘顺序也决定了计算量的大小。
我们用利用动态规划的方式(dp[i][j]表示第i个矩阵至第j个矩阵这段的最优解,还有对于两个矩阵A(i,j)*B(j,k)则需要i*j*k次乘法),推出状态转移方程:
dp[i][j]
=
0;
(i
==j,表示只有一个矩阵,计算次数为0)
dp[i][j]
=
mindp[i][k]
+
dp[k+1][j]
+
p[i-1]*p[k]*p[j];
(i<j
&&
i<=k<j)
dp[1][n]即为最终求解.
View
Code

从暴力递归到动态规划小乖,你也在为转移方程而烦恼吗?

前引:继上篇我们讲到暴力递归的过程,这一篇blog我们将继续对从暴力递归到动态规划的实现过程,与上篇类似,我们依然采用题目的方式对其转化过程进行论述。

上篇博客:https://blog.csdn.net/m0_65431718/article/details/129604874?spm=1001.2014.3001.5502

一.n皇后问题

八皇后问题是十九世纪著名的数学家高斯于1850年提出的。问题是:在n×n的棋盘上摆放n个皇后,使任意两个皇后都不能处于同一行、同一列或同一斜线上。

我们的解题思路如下:采用暴力递归,既然要求任意两个皇后不能在同一行和同一列和同一斜线,我们依次对这三者进行讨论:①同一行:每一层递归代表一行,我们只要保证在每一层递归中只放置一个皇后即可②同一列:按照题目的要求,我们在访摆放第n层的皇后时,要保证它和前面n-1等皇后都不冲突,这就意味着我们在进行下一层递归的时候仍能有方法访问前面皇后摆放的位置:我们的第一考虑对象当然是数组,但是比较巧妙的是它是一个n*n的棋盘,第一个n代表行,第二个n代表列,我们只需要建立一个长度为n的一维数组,下标代表第几行,下标对应的数组元素代表列,作为参数带入到下一层递归中,斜线也是如此,我们详细展开说说列和斜线的要求:对于列来说,我们要遍历缓存,使前面的缓存和当前列不相等即为不冲突,对于斜线的要求来说,对于一个正方形棋盘,我们其实很容易想到的是直线的斜率为1,也就是说两个元素行的变化如果等于列的变化,我们可以说在同一条斜线上。我们根据思路给出code:

public class NEmpress 
    public static void main(String[] args) 
        //创建date
        int n=4;
        int[]data=new int[n];
        System.out.println(process(data, 0, n));
       

    
    //创建递归过程
    public static int process(int[]data,int i,int n)
        //判断出口条件:共有n个元素,一旦当前行越过了n-1,则说明成功
        if(i==n)
            return 1;
        
        //循环处理每一列
        //如果没结束
        int res=0;
        for(int j=0;j<n;++j)
            //判断当前元素是否有效
            if(isValid(data,i,j))
                data[i]=j;
                //进入下一层递归
               res+= process( data, i+1, n);
            
        
        return res;
    
 private static boolean isValid(int[] data, int i, int j) 
        //在data中检验是否存在
        for(int k=0;k<i;++k)
            //第一个是判断从0到i-1行中列的元素是否相等,第二个是判断是否在同一斜线
            if(data[k]==j||(Math.abs(i-k)==Math.abs(j-data[k])))
                return false;
            
        
        return true;
    

如果不改变问题的实现思路,很难去实现大的效率提升,但是考虑不同的方法仍能在一定程度上提升效率(常数级提升):采用位运算,总体的实现逻辑和之前的暴力递归完全相同,但是就具体细节做出了一定的改动,我们给出递归的核心代码,并改动进行解释说明:

limit:是位数限制,对于行列数为N的棋盘,limit的限制是:对于前N个二进制位数均为1,对于N行列的棋盘而言,前N个二进制位代表棋盘的每一行(第一个二进制位代表第一行,第二个代表第二行........)

①col:对于每次摆放个皇后,就将这个二进制位置变为1,表示这个二进制位不能摆放皇后了

②leftLim:左斜线限制,如果leftLim为1,代表对于当前行来说,这个位置不能摆放皇后了。

③RightLim:右斜线限制,如果RightLim为1,同样对于当前行来说,这个位置不能摆放皇后了。

④limit==col:代表col前N个二进制位都是1,表示N个皇后都已经摆放好了,游戏结束,退出递归

⑤limit&(~(col|leftLim|rightLim)):pos是在每一行中能选择的列

⑥ mostRight=pos&(~pos+1):取出最右边的一位

⑦ pos-=mostRight:将最右边的位置从可选择的位数中去除,使当前行不能放置皇后

⑧while(pos!=0) 循环当前行中能选择的位置

⑨res+= process2(limit,col|mostRight,(leftLim|mostRight)<<1, (rightLim|mostRight)>>>1):循环下一层

public static int process2(int limit,int col,int leftLim,int rightLim)
        //递归出口
    if(limit==col)
        return 1;
    
    //计算能放的位置:
    int pos= limit&(~(col|leftLim|rightLim));
    int mostRight=0;
   int res=0;
    //检验是否能递归
    while(pos!=0)
        //找最右的位置
         mostRight=pos&(~pos+1);
 
        pos-=mostRight;
      res+=  process2(limit,col|mostRight,(leftLim|mostRight)<<1, (rightLim|mostRight)>>>1);
    
    return res;

我们对代码中的几个点进行解释说明:

三.机器人走路问题:(从暴力递归到动态规划的实践)

问题要求如下:

1.假设有排成一行的n个位置记为1-N,N一定大于等于2
2.开始时机器人在m位置上,m一定是1-N中的一个
3.机器人要在规定的步数到达指定的终点,计算到达指定终点的路线有多少条
4.如果机器人来到1位置只能往右来到2位置
5.如果机器人来到N位置只能往左来到N-1位置
6.如果机器人在其他位置,则机器人可以往右也可以往左

对于暴力递归,实现思路就相对比较简单:对于当前位置而言,如果位置是1,他只能选择2,如果在2-N-1的位置,他可以向左和向右走,如果在N位置,只能往N-1的位置走,不断走,直到剩余步数为0,判断是不是要求的位置,然后返回结果。

我们给出关于暴力递归的代码:

public int walking(int N,int cur,int rest,int P)
        //编写递归出口
        if(rest==0)
            if(cur==P)
                return 1;
            else 
                return 0;
            
        
        //排除特殊情况,在0位置处:只能往后走
        if(cur==1)
            return walking(N, cur+1, rest-1, P);
        
        //在最后一个位置,只能往前走
        if(cur==N)
            return walking(N, cur-1, rest-1, P);
        
        //在中间,可以往前往后走
        return walking(N, cur-1, rest-1, P)+walking(N, cur+1, rest-1, P);


        

为什么说这是从暴力递归到动态规划的实践开始呢?我们对此进行解释:

我们能在暴力递归的基础上修改为动态规划,什么是动态规划呢?是将暴力递归中重复计算的过程转化为缓存,从而降低暴力递归中重复计算的次数,转而从相关缓存中获取,是一种典型的空间换时间的策略,对于动态规划而言,其实最难的部分是写出关于动态规划的转移方程。

对这道题来说,这种动态规划的类型是记忆性搜索:如果这个位置有缓存,就直接返回缓存结果,否则递归。

动态规划的的code如下:

public int walkCache(int N,int cur,int rest ,int [][]dp,int P)
        if(dp[cur][rest]!=-1)
            return dp[cur][rest];
        
        if(rest==0)
            dp[cur][rest]=cur==P?1:0;
            return dp[cur][rest];
        
        if(cur==1)
            dp[cur][rest]=walkCache(N, cur+1, rest-1, dp,P);
            return dp[cur][rest];
        
        if(cur==N)
            dp[cur][rest]=walkCache(N, cur-1, rest-1, dp,P);
            return dp[cur][rest];
        
            return dp[cur][rest]=walkCache(N, cur-1, rest-1, dp,P)+walkCache(N, cur+1, rest-1,dp, P);
        
    

四.零和博弈问题:

问题描述:

思路:对于A而言,作为先手,他一定在纵观全局后选择对自己最有利的计划,而B作为后手,只能在A 选择之后在此基础上选择对自己最友好的计划和策略,换句话说,B选择的只能是A选择剩下的

所以我们需要设计两个函数,一个是先手函数,选择其中相对自己而言最优的策略,即为选择自己能选的棋中的最大值,而同样需要设计一个后手函数,它的作用是:在后手参与下选择相对较小的选择(只能选择A选剩下的

我们给出code:

package violencerecursion;

/**
 * @author tongchen
 * @create 2023-03-21 16:09
 */
public class GameWin 
    public static void main(String[] args) 
        GameWin gameWin = new GameWin();
        int[]arr=1,100,1;
        System.out.println(gameWin.win(arr));
    
    public int win(int[]arr)
        //零和博弈问题,解题思路:先手的人拿最优的选择,后手的人只能被迫接收最差的结果
        int left=0;
        int right= arr.length-1;
       return Math.max(f(arr, 0, arr.length-1),s(arr, 0, arr.length-1));
    

    private int f(int[] arr, int left, int right) 
        //递归出口
        if(left==right)
            return arr[left];
        
        //选择已知策略中最优的选择
        return Math.max(arr[left]+s(arr,left+1,right),arr[right]+s(arr,left,right-1));

    

    private int s(int[] arr, int left, int right) 
        if(right==left)
            return 0;
        
        //B相当于从第二个棋子先选择(因为第一个棋子肯定被A选走了,B先手选第二个棋子)
        //但是这种情况下B只能选择在A纵观全局后选择最优策略之后被迫选择劣的选择(即最小值)
        return Math.min(f(arr, left+1, right),f(arr, left, right-1));
    

后续会更新关于动态规划的转移方程的编写思路过程,希望大家能持续关注捏~

以上是关于动态规划如何去找动态转移方程的主要内容,如果未能解决你的问题,请参考以下文章

动态规划(下):如何求得状态转移方程并进行编程实现?

动态规划--矿工挖矿

转载 HDU 动态规划46题只提供思路与状态转移方程

动态规划(上):如何实现基于编辑距离的查询推荐?

动态规划初步

浅谈动态规划(个人理解)