动态规划面试题型归类机

可用动态规划解决的问题的特性是:任何一个时刻(或步骤)的状态,都可被有限个之前状态所表示。难点就是需要寻找出问题中隐藏的状态,并准确地建模转换关系。

1. 斐波那契数列的第n项

def Fibonacci(self, n):
    if n==0: return 0
    if n==1: return 1
    a, b, c = 0, 1, -1
    for i in range(2, n + 1):
        c = a + b
        a = b
        b = c
    return c

2. 跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

方法:第一步找到适用于该题的状态表示,这里把状态函数设定为f(n),即跳了n个台阶的跳法总数。
现在需要通过先前的信息推导f(n),假设青蛙跳了n格,如果它是1跳到n格的,
那么一共f(n-1)种跳法;如果是2跳到n格的,那么一共f(n-2)种跳法,所以一共有f(n-1)+f(n-2)种跳法。

[f(n)= egin{cases} 1 & n=1 \ 2 & n=2 \ f(n-1) + f(n-2) & n>2 end{cases} ]

3. Triangle

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]
给以上这种三角形,返回从顶到底的元素和最小走法(每次只能走邻近子节点)
比如以上的结果:2 + 3 + 5 + 1 = 11

方法:第一步找到状态,可以设定从三角形某行某列到底部的元素和最小走法的元素和大小为dp[row][i]。
比如从2到底结果状态表示为dp[0][0],其可以被拆解成 min(从3到底的结果dp[1][0], 从4到底的结果dp[1][1]) + 2
状态转换关系即可确定: 

[dp[row][i] = min(dp[row+1][i], dp[row+1][i+1]) + triagnle[row][i] ]

4. Minimum Path Sum

Input:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
Output: 7
非负二维数组,找到从左上角到右下角的路径,使得元素和最小
上图这个路径 1→3→1→1→1 元素和最小, 为7

方法:设定状态dp[row][col]表示从左上角走到row行col列元素和最小的路径的元素和大小。
状态转移方程为:(需要剪枝)

[dp[row][col] = min(dp[row][col-1], dp[row-1][col]) + matrix[row][col] ]

5. Integer Break

Input: 10
Output: 36
Explanation: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36.
给一个正数,怎么拆分成至少两个数的和形式,使分解出来的元素积组大。

方法:设定状态dp[n]表示输入为n时的结果(最大积的拆分方法的积)。
(这里状态约束条件很弱,i需要从1到n-1取,因此复杂度也需要O(n^2))
状态转移方程为:

[dp[n] = max(i*dp[n-i], i*(n-i)), i in 1,2,...,n-1 ]

int integerBreak(int n) {
    int dp[n];
    dp[1] = 1;
    dp[2] = 1;
    for(int i=3; i<=n; i++){
        dp[i] = -1;
        for(int j=1; j<i; j++){
            dp[i] = max(j*dp[i-j], max(j*(i-j), dp[i])); //注意后面的dp[i]
        }
    }
    return dp[n];
}

6. Perfect Squares

Input: n = 13
Output: 2
Explanation: 13 = 4 + 9.
输入一个正整数n, 输出其最少能由几个完全平方数相加组成(完全平方数为1,4,9,16,...)

方法:跟上题类似,设定状态dp[n]为整数n的输出结果,状态转移方程为

[dp[n] = min(dp[n-s]+1), s in 1,4,9,16,... ]

int numSquares(int n) {
    int *dp = new int[n + 1];
    dp[0] = 0;
    for (int i=1; i<=n; i++) {
        int _min = INT_MAX;
        for (int j=1; j*j<=i; j++) {
            _min = min(_min, dp[i-j*j]+1);
        }
        dp[i] = _min;
    }
    delete[]dp;
    return dp[n];
}

7. Decode Ways

'A'到'Z'用数字1~26表示
Input: "12"
Output: 2
12可以有两种编码方式 "AB" (1 2) or "L" (12).

方法:设输入为n的结果为dp[n],输入串为s,现在考虑dp[n]怎么用先前信息表示。
- 当s[n-2]与s[n-1]的组合为'10'或'20'时,由于没有单独0的编码方式,
  因此一共dp[n-2]种
- 当s[n-2]与s[n-1]的组合在11到19或21到26,那么要么组合成整体要么分开来,
  因此一共dp[n-2]+dp[n-1]种
- 当s[n-2]与s[n-1]的组合在01到09时,最后一位一定自成一体,
  因此一共dp[n-1]种
def numDecodings(self, s):
    if len(s)==0 or s[0]=='0': return 0
    dp = [1,1]
    for i in range(2, len(s)+1):
        if s[i-2:i]=='10' or s[i-2:i]=='20':
            dp.append(dp[i-2])
        elif 10<int(s[i-2:i])<=26:
            dp.append(dp[i-1]+dp[i-2])
        elif s[i-1]!='0':
            dp.append(dp[i-1])
        else:
            return 0
    return dp[-1]

8. Unique Paths

给一个m*n矩阵,从左上角到右下角一共多少条路径,要求只能走右边或者左边

方法:设定状态dp[row][col]为在row行col列的路径数,状态转移方程为

[dp[row][col] = dp[row-1][col] + dp[row]p[col-1] ]

def uniquePaths(self, m, n):
    dp = [[1 for i in range(m)] for i in range(n)]
    for i in range(1, n):
        for j in range(1,m):
            dp[i][j] = dp[i-1][j] + dp[i][j-1]
    return  dp[-1][-1]

9. Unique Paths II

在上题基础上加了障碍,下面1是障碍
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
Output: 2
1. Right -> Right -> Down -> Down
2. Down -> Down -> Right -> Right

方法:在上题基础上状态转换关系还要考虑障碍情况,直接上代码
def uniquePathsWithObstacles(self, obstacleGrid):
    # type obstacleGrid: List[List[int]]
    row = len(obstacleGrid)
    col = len(obstacleGrid[0])
    dp = [[1] * col for i in range(row)]
    for i in range(0, row):
        for j in range(0, col):
            if obstacleGrid[i][j]: # 如果这个点是障碍
                dp[i][j] = 0
            elif i == 0:
                dp[i][j] = dp[i][j-1]
            elif j == 0:
                dp[i][j] = dp[i-1][j]
            else:
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
    return dp[-1][-1]

10. House Robber

给一个自然数数组,在不允许相邻取的情况下,求可取的最大和
Input: [1,2,3,1]
Output: 4
取1,3和为4

方法:设定状态dp[n]表示前n项在不能相邻取情况下最大和取法的最大和(结果),要用前面信息表示dp[n]

[dp[n] = max(value[n]+dp[n-2], dp[n-1]) ]

def rob(self, nums):
    n =  len(nums);
    if n==0: return 0
    if n==1: return nums[0]
    if n==2: return max(nums[0], nums[1])
    dp = [0 for x in range(n)];
    dp[0] = nums[0]
    dp[1] = max(nums[0], nums[1])
    for i in range(2, n):
        dp[i] = max(nums[i]+dp[i-2], dp[i-1])
    return dp[-1]

11. House Robber ||

在上题的基础上,输入数组是一个环形的(即最后一个和第一个视为相邻)
Input: [2,3,2]
Output: 3

方法:由于第一个和最后一个相邻,那么有如下两种情况:
1. 取第一个的时候,不能取最后一个,相当于把最后一个从数组中去掉
2. 取最后一个的时候,不能取第一个,相当于把第一个从数组中去掉
这样,问题就简化为上一道题的两种输入的叠加形式
def rob(self, nums):
    if len(nums) == 0: return  0
    if len(nums) == 1: return  nums[0]
    if len(nums) == 2: return  max(nums)
    def getRob(start, stop, nums):
        # 上一题的解法
        n = nums[start: stop + 1]
        dp = [0] * len(n)
        dp[0], dp[1] = n[0], max(n[0], n[1])
        for i in range(2, len(n)):
            dp[i] = max(dp[i - 1], dp[i - 2] + n[i])
        return dp[-1]
    return  max(getRob(0, len(nums) - 2, nums), getRob(1, len(nums) - 1, nums))

12. House Robber |||

在第10题基础上,变成二叉树形式了,不能取直接相邻的
Input: [3,2,3,null,3,null,1]
     3
    / 
   2   3
        
     3   1
Output: 7 
(3 + 3 + 1 = 7)

方法:跟第10题类似,只是加了个dfs来遍历树,下面给出递归法代码(这种树结构不好用迭代法)
class Solution {
public:
    int rob(TreeNode* root) {
        unordered_map<TreeNode*, int> m;
        return dfs(root, m);
    }
    int dfs(TreeNode *root, unordered_map<TreeNode*, int> &m) {
        if (!root) return 0;
        if (m.count(root)) return m[root];
        int val = 0;
        if (root->left) {
            val += dfs(root->left->left, m) + dfs(root->left->right, m);
        }
        if (root->right) {
            val += dfs(root->right->left, m) + dfs(root->right->right, m);
        }
        val = max(val + root->val, dfs(root->left, m) + dfs(root->right, m));
        m[root] = val;
        return val;
    }
};

13. Best Time to Buy and Sell Stock

给一个整数组代表某股票每天的价格,假设最多只能买卖一次,并且一天只能选择 买卖不动 三种操作,求最大化收益
[7, 1, 5, 3, 6, 4],在价格为1的时候买入,在价格为6的时候卖出,可以得到最大盈利值为5。(5 = 6 - 1)
[7, 6, 5, 4, 3, 2],选择不买不卖,最大盈利值为0。

方法:第一步求差分(a[i]=a[i]-a[i-1])然后就转化为最大化连续子序列和问题,解法非常经典。
设定状态dp[i]表示以位置i为结尾的最大子序列和,如果数组记为a,那么状态转移方程是:
(由于整个数组中的最大子序列和必有一个位置为结尾,因此最终结果为dp数组的最大值。
与先前问题不同,这个问题的状态表示比较隐晦,需要迂回一下。)

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

int maxProfit(vector<int>& prices) {
    if(prices.size()==0) return 0;
    vector<int> profits(prices.size());
    profits[0]=0;
    for(int i=1; i<profits.size(); i++)
        profits[i] = prices[i]-prices[i-1]; //profits 储存差分
    int dp[profits.size()] = {0}, maxprofit = 0;
    for(int i=1; i<profits.size(); i++){
        dp[i]=max(profits[i], dp[i-1]+profits[i]);
        maxprofit=max(maxprofit, dp[i]);
    }
    return maxprofit;
}

14. Best Time to Buy and Sell Stock with Cooldown

在上题情境下,现在规定:
1. 有无限次交易机会,但只能遵循 买入->卖出->买入->卖出 顺序,再自己已有股票没卖出之前不得再买入
2. 卖出与买入之间起码要有一个间隔
Input: [1,2,3,0,2]
Output: 3 
Explanation: transactions = [buy, sell, cooldown, buy, sell]

方法:这里应用了多个状态表示
- 状态hold[i]表示截止第i天,仍然持有股票,这时的最大化收益
- 状态sold[i]表示截止第i天,刚好卖出股票,这时的最大化收益
- 状态rest[i]表示截止第i天,已经卖出股票并且第i天不操作,这时的最大化收益
int maxProfit(vector<int>& prices) {
    int len = prices.size();
    if(len <= 1) return 0;
    vector<int> hold(len, 0); 
    vector<int> sold(len, 0); 
    vector<int> rest(len, 0); 
    hold[0] = -prices[0];
    for(int i=1; i<len; i++){
        // 第i天为卖出状态,最大化收益就是:如果前一天是持有状态的收益+这天价格
        sold[i] = hold[i-1] + prices[i]; 
        // 第i天为休息状态,最大化收益分两种情况:要么前一天也是休息的,要么前一天是卖出状态
        rest[i] = max(rest[i-1], sold[i-1]);
        // 第i天为持有状态,最大化收益分两种情况:要么前一天也是持有,要么前一天是休息
        hold[i] = max(hold[i-1], rest[i-1]-prices[i]);
    }
    return max(sold[len-1], rest[len-1]);
}

15. 0-1背包问题

有两个数组w和v,分别记录物品的重量和价值,如果背包重量不能超过m,求一共能达到的最大的价值

方法:设定状态dp[i][c]表示到i号物体截止做选择,重量限制为c的情况下能达到的最大总价值。
现在需要用一个同向(要么都加要么都减)的状态来表示此状态,转换方程如下

[dp[i][c] = max(dp[i-1][c], v[i]+dp[i-1][c-w[i]]) ]

const int weight[] = { 2,3,4,5 };
const int value[] = { 3,4,5,6 };
const int n = 4;
const int capacity = 8;

// solve
vector<vector<int>> dp(n, vector<int>(capacity + 1, 0));
for (int i = weight[0]; i <= capacity; i++) dp[0][i] = value[0]; // init
for (int i = 1; i < n; i++) {
    for (int c = 0; c <= capacity; c++) {
        if (c < weight[i]) 
            dp[i][c] = dp[i - 1][c];
        else
            dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - weight[i]] + value[i]);
    }
}
cout << dp[n - 1][capacity] << endl;

// route
int i = n - 1, j = capacity;
while(true) {
    if (i == 0) {
        if (dp[i][j] != 0) cout << i << endl;
        break;
    }
    if (dp[i][j] == dp[i - 1][j]) i--;
    else if (j - weight[i] >= 0 && dp[i][j] == dp[i - 1][j - weight[i]] + value[i]) {
        cout << i << endl;
        j -= weight[i];
        i--;
    }
}

16. Partition Equal Subset Sum

判断一个正数数组可不可以被拆分为两个子集,使得他们的和相等
Input: [1, 5, 11, 5]
Output: true
可被拆分为 [1, 5, 5] 和 [11].

方法:判断是否成立等价于,判断这个数组中是否有子集的和为总和的一半,
设定状态dp[i][j]表示,数组前i项中是否存在子集的和为j,现在需要用前向的状态表示dp[i][j],转换关系为

[dp[i][j] = dp[i-1][j] || dp[i-1][j-x[i]] ]

这道题是0-1背包问题的变形,迭代法代码如下

bool canPartition(vector<int>& nums) {
    int sum = accumulate(nums.begin(), nums.end(), 0), target = sum >> 1;
    if (sum & 1) return false;
    if (nums.size()==1) return false;
    vector<vector<bool>> dp(nums.size(), vector<bool>(target+1)); 
    for(int i=0; i<nums.size(); i++) {
        for(int j=0; j<=target; j++) {
            dp[i][j] = false;
            dp[i][0] = true;
        }
    }
    dp[0][nums[0]] = true;
    for(int i=1; i<nums.size(); i++) {
        for (int j=nums[i]; j<=target; j++) {
            dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
        }
    }
    return dp[nums.size()-1][target];
}

由于上面方法需要开二维数组比较麻烦,下面对其进行优化

bool canPartition(vector<int>& nums) {
    int sum = accumulate(nums.begin(), nums.end(), 0), target = sum >> 1;
    if (sum & 1) return false;
    if (nums.size()==1) return false;
    vector<bool> dp(target+1, false); 
    for(int i=0; i<=target; i++) 
        dp[i] = i == nums[0];
    for(int i=1; i<nums.size(); i++) {
        // 注意,此时下面需要从后往前迭代,因为每一个dp[j]的计算都依赖于前面项
        for (int j=target; j>=nums[i]; j--) {
            dp[j] = dp[j] || dp[j-nums[i]];
        }
    }
    return dp[target];
}

17. Coin Change

有一些特定面值的货币,如[1,2,5]
现在给定一个价格,如11,最少使用的货币数。
以上输出为3,因为5+5+1=11,如果不能找零输出-1

方法:与上面第6题类似,设定dp[n]为输入为n的结果,状态转移方程为

[dp[n] = min(dp[n-s]+1), s in x[0], x[1], .. ]

代码也基本类似,注意这里不能找零-1的处理方法,代码如下

int coinChange(vector<int>& coins, int amount) {
    const int int_max = 99999999;
    vector<int> dp(amount+1, int_max);
    dp[0] = 0;
    for(int i=1; i<=amount; i++) {
        int _min = int_max;
        for(int j=0; j<coins.size(); j++) {
            if (i-coins[j]>=0)
                _min = min(_min, dp[i-coins[j]]+1);
        }
        dp[i] = _min;
    }
    return dp[amount] == int_max ? -1 : dp[amount];
}

18. Combination Sum IV

给一个正整数字典,如[1,2,3],以及一个整数如4
找出整数可被字典元素组成排列组成和的个数,如以上可能组合为
(1, 1, 1, 1)、 (1, 1, 2)、 (1, 2, 1)
(1, 3)、 (2, 1, 1)、 (2, 2)、 (3, 1)
因此输出为7

方法:为上面第2题的变形,使用完全背包方法做,代码如下
int combinationSum4(vector<int>& nums, int target) {
    vector<unsigned long long> dp(target+1, 0);
    dp[0] = 1;
    for(int i=1; i<=target; i++) {
        for(int j=0; j<nums.size(); j++) {
            if (i-nums[j]>=0) 
                dp[i] += dp[i-nums[j]];
        }
    }
    return dp[target];
}

19. Ones and Zeroes

给一个以01组成的字符串列表,如["10", "0001", "111001", "1", "0"],
给两个值m=5和n=3,
找到有m个0和n个1的最多单元数方法的单元数(列表中每一项只用一次)
上面的结果就是4,为"10","0001","1","0"

方法:由于不可以重复取,属于0-1背包问题,
设定状态dp[i][m][n]为列表前i个情况下m个0和n个1时的输出情况,,那么状态转换关系为

[dp[i][m][n] = max(dp[i-1][m][n], dp[i-1][m-count_0(x[i])][n-count_1(x[i])]+1) ]

int count(string s, char ch) {
    int count = 0;
    for(int i=0; i<s.size(); i++) if(s[i]==ch) count++;
    return count;
}

int findMaxForm(vector<string>& strs, int m, int n) {
    vector<int> count_0(strs.size());
    vector<int> count_1(strs.size());
    for(int i=0; i<strs.size(); i++) {
        count_0[i]= count(strs[i], '0');
        count_1[i]= count(strs[i], '1');
    }
    vector<vector<vector<int>>> dp(strs.size(), 
        vector<vector<int>>(m+1, vector<int>(n+1, 0)));
    dp[0][count_0[0]][count_1[0]] = 1;
    for(int i=1; i<strs.size(); i++) {
        for(int j=0; j<=m; j++) {
            for(int k=0; k<=n; k++) {
                if ((j>=count_0[i]) && (k>=count_1[i]))
                    dp[i][j][k] = max(dp[i-1][j][k], 
                        dp[i-1][j-count_0[i]][k-count_1[i]]+1);
                else
                    dp[i][j][k] = dp[i-1][j][k];
            }
        }
    }
    return dp[strs.size()-1][m][n];
}

但这种方法比较吃内存,因此需要优化

int findMaxForm(vector<string>& strs, int m, int n) {
    vector<int> count_0(strs.size());
    vector<int> count_1(strs.size());
    for(int i=0; i<strs.size(); i++) {
        count_0[i]= count(strs[i], '0');
        count_1[i]= count(strs[i], '1');
    }
    vector<vector<int>> dp(m+1, vector<int>(n+1, 0)); // 变成二维
    for(int i=0; i<strs.size(); i++) {
        // 注意,这里j与k需要从后往前迭代,因为是前向依赖关系
        for(int j=m; j>=0; j--) {
            for(int k=n; k>=0; k--) {
                if ((j>=count_0[i]) && (k>=count_1[i]))
                    dp[j][k] = max(dp[j][k], dp[j-count_0[i]][k-count_1[i]]+1);
                else
                    dp[j][k] = dp[j][k];
            }
        }
    }
    return dp[m][n];
}

20. Word Break

给一个字符串字典,如 wordDict=["leet", "code"]
在给一个字符串如 s="leetcode",判断该字符串是否可用这个字典中的串构成,
以上为true,这里字典中的串可以重复使用

方法:设定状态dp[i]为字符串中前i个字符是否可以用字典构成,状态转移方程为

[dp[i] = egin{cases} OR(dp[i-len(key)]), key in wordDict & s[i-len(key):key]==key \ False & otherwise \ end{cases} ]

该题为完全背包问题,迭代法代码如下

def wordBreak(self, s, wordDict):
    # s: str
    # wordDict: List[str]
    wordDict = list(set(wordDict))
    dp = [True] + [False] * (len(s))
    for i in range(1, len(s)+1):
        for key in wordDict:
            if ((i-len(key))>=0) and (s[i-len(key):i]==key):
                dp[i] = dp[i] or dp[i-len(key)]
    return dp[-1]

21. Target Sum

给一个非负整数组成的列表如[1, 1, 1, 1, 1], 再给一个数字如3
判断有多少种方法能使列表中数字用+或-连接能算出这个数字 
上述例子输出为5,列举如下
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

方法:设定dp[i][j]表示到列表前i号数字为止,能凑出数字j的方法数,为0-1背包问题,状态转化方程如下

这里有这么几个问题:
- 数组中负索引表示问题,通过一个偏置并且开更大的数组或使用map映射解决。
- 这里的j索引位置迭代方向不定,但是由于j的变化是离散的,不影响。
按照上面公式,可以用一个复杂度较高的解法

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

int findTargetSumWays(vector<int>& nums, int S) {
    int offset = 100000, n = nums.size(), sum = accumulate(nums.begin(), nums.end(), 0);
    if ((S>offset) || (S<-offset)) return 0;
    vector<vector<int>> dp(n, vector<int>(2*offset+2, 0)); // 开一个较大的dp缓存
    dp[0][-nums[0]+offset] += 1;
    dp[0][+nums[0]+offset] += 1;
    for (int i=1; i<n; i++) {
        for (int j=-sum; j<=sum; j++) {
            dp[i][j+offset] = dp[i-1][j-nums[i]+offset] + dp[i-1][j+nums[i]+offset];
        }
    }
    return dp[n-1][S+offset];
}

22. Longest Increasing Subsequence (LIS)

给一个无序的序列, 求最长递增子序列长度
如[10,9,2,5,3,7,101,18]的最长递增子序列为[2,3,7,101]因此为4

方法:设定dp[i]表示以位置i为结尾的最长递增子序列长度,状态转移方程是:

[dp[i] = max(1, dp[j]+1), s.t. j<i and x[i]>x[j] ]

与13题解法类似, 迭代法代码如下

int lengthOfLIS(vector<int>& nums) {
    if (nums.empty()) return 0;
    vector<int> dp(nums.size(), 0);
    dp[0] = 1;
    int res = 1;
    for (int i=1; i<nums.size(); i++) {
        int _max = 1;
        for (int j=0; j<i; j++) 
            if(nums[i]>nums[j]) 
                _max = max(_max, dp[j]+1);
            dp[i] = _max;
        res = max(res, dp[i]);
    }
    return res;
}

23. Wiggle Subsequence

求最长摆动子序列长度, 即子序列必须一大一小或者一小一大排列
[1,17,5,10,13,15,10,5,16,8]最长摆动子序列为[1,17,10,13,10,16,8]长度为7

方法:设定up[i]表示前i项以上升沿结束的最长摆动子序列长度, down[i]表示前i项以下降沿结束的最长摆动子序列长度。
为什么不设定状态为以i项结尾上升或下降沿的最长摆动子序列长度(LIS做法)?因为这道题贪心可解。
int wiggleMaxLength(vector<int>& nums) {
    int sz = nums.size();
    if(!sz) return 0;
    vector<int> up(sz, 0), down(sz, 0);
    up[0] = down[0] = 1;
    for(int i=1; i<sz; i++) {
        if(nums[i] > nums[i-1]) {
            up[i] = down[i-1] + 1;
            down[i] = down[i-1];
        }
        else if(nums[i] < nums[i-1]) {
            down[i] = up[i-1] + 1;
            up[i] = up[i-1];
        }
        else {
            up[i] = up[i-1];
            down[i] = down[i-1];
        }
    }
    return max(up[sz-1], down[sz-1]);
}

24. 最长公共子序列 (LCS)

如S1=ABCD, S2=AEBD, 那么S1与S2的最长公共子序列是ABD, 长度为3

方法:设定dp[i][j]表示第一个串前i第二个串前j的最长公共子序列,转移方程为

[dp[i][j] = egin{cases} dp[i-1][j-1]+1 & x_i = y_j \ max(dp[i-1][j], dp[i][j-1]) & x_i eq y_i \ end{cases} ]

25. Maximum Length of Repeated Subarray

最长重复子序列
A: [1,2,3,2,1]
B: [3,2,1,4,7]
最长重复子序列为[3,2,1]长度为3

方法:与上题LCS不同这里求完全匹配串(Longest Common Substring);
设定dp[i][j]表示以第一个串前i截止,以第二个串前j截止的最长重复子序列长度,转移方程为

[dp[i][j] = egin{cases} dp[i-1][j-1]+1 & x_i = y_j \ 0 & x_i eq y_i \ end{cases} ]

迭代法代码如下

int findLength(vector<int>& A, vector<int>& B) {
    int res = 0, m = A.size(), n = B.size();
    vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            dp[i][j] = (A[i - 1] == B[j - 1]) ? dp[i - 1][j - 1] + 1 : 0;
            res = max(res, dp[i][j]);
        }
    }
    return res;
}

26. Edit Distance

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
1. 插入一个字符
2. 删除一个字符
3. 替换一个字符

word1 = "horse", word2 = "ros" -> 3
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

方法:设定dp[i][j]表示第一个串前i个和第二个串前j个的最少操作数
如果x[i]==y[j], dp[i][j]与dp[i-1][j-1]; 否则有三种操作可能
1. x[i-1]==y[j-1]替换操作, 1+dp[i-1][j-1]
2. x[i]==y[j-1]在x[i]后面加上y[j], 1+dp[i][j-1]
3. x[i-1]==y[j]删除x[i],1+dp[i-1][j]
int minDistance(string word1, string word2) {
    int m = word1.length(), n = word2.length();
    vector<vector<int>> dp(m+1, vector<int>(n+1));
    for (int i = 0; i <= m; i++) dp[i][0] = i;
    for (int j = 0; j <= n; j++) dp[0][j] = j;
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (word1[i-1] == word2[j-1])
                dp[i][j] = dp[i-1][j-1];
            else
                dp[i][j] = 1 + min(dp[i-1][j-1], min(dp[i][j-1], dp[i-1][j]));         
        }
    return dp[m][n];
}
原文地址:https://www.cnblogs.com/xytpai/p/13502563.html