备战练习(五)(动态规划)

介绍这篇时, 我会用简单讲讲动态规划的思想, 毕竟动态规划的思想是很重要的。
做这种题有种感觉, 你会思想,推出状态, 状态转移方程, 问题自然游刃而解。

如果推不出,这种题真心很难,无从下手。

一、将这种题的做法分为两步:
1、从实际问题看规律,推出状态、状态转移方程。(这种是最难的、也是最关键的。)
2、编写代码

二、动态规划问题特点
这种题的特点,大问题可以划分小问题, 小问题可以划分为更小的问题, 每个问题解决方法都是类似的。

举例简单的斐波那契数列。[1,1,2,3,5,8,13,21…]
1、状态: F(i) (代表求第i个斐波那契数)
2、状态转移方程: F(i) = F(i-1) + F(i-2) (跟前两项有关)

当然i=1 或 i=2为特殊的, 可以直接处理。
当i>2: F(i) = F(i-1) + F(i-2)

我们以推导F(6)为例。
F(6) = F(5) + F(4);
F(5) = F(4) + F(3);
F(4) = F(3) + F(2);
我们求解F(6), 依赖前两项F(5), F(4);
求解F(5),又依赖前两项。
验证了: 大问题可以划分为小问题, 而且问题之间解决方式类似的。

其实动规问题解决还需要开辟额外的空间, 标志之前求解的值。
斐波那契这道题其实也是需要的, 但是每一项的值只跟前两项有关, 因此可以不用开辟空间保存F(i),i之前所有的求解。 只需要保存前两项就行。

总结一下,动规问题的处理分为几点:
1、确定状态F(i)
2、找到状态转移方程
3、开辟辅助空间保存之前的值。

还是那句话,思想难找, 得多做题,有那种感觉。

一、word-break(字符串切割问题)

题目链接

给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。
例如:
给定s=“leetcode”;
dict=[“leet”, “code”].
返回true,因为"leetcode"可以被分割成"leet code".

分析:

1、状态F(i) : 代表着[0,i]的字符串是否能被切割。
{ F(3), "lee"字符串能不能切割。 F(4),"leet"字符串能不能切割。}

2、状态转移方程F(i) = F(j) && F(j,i) (0<=j < i)
代表F(0,j) && F(j,i), (j之前的情况 && j到i之间字符串情况)

当添加一个新的字符时,
可能会存在添加了这个字符, 整体可以分割。(F(3)->F(4)时,相当于添加了一个字符’t’, leet整体可以被分割。 ) 对应着F(0) && F(0,4)的情况。
也可能存在F(1) && F(1,4)也可以成立的情况, F(2) && F(2,4) 到 F(3) && F(3,4)
之间的情况都是成立的问题, 因此都需要考虑。

#include <unordered_set>
 
class Solution {
public:
    bool wordBreak(string s, unordered_set<string> &dict) {
        //开辟辅助空间 
        vector<bool> vec(s.size() + 1, false);	//初始化全为false。
        
        //F(0) = true, 方便后续操作。 当然这种情况也是不存在的。
        vec[0] = true;
         
        for(int i = 1; i <= s.size(); i++) {
        	//F(i) = F(j) && F(j,i) (0 <= j < i)
            for(int j = 0; j < i; j++) {
            	//F(j) && F(j,i)
                if(vec[j] && dict.find(s.substr(j, i - j)) != dict.end() ){
                    vec[i] = true;	//都存在时, F(i)置为true.
                    break;
                }
            }
        }
         
        return vec[s.size()];
    }
};

二、unique-paths(机器人路径)

题目链接

一个机器人在m×n大小的地图的左上角(起点,下图中的标记“start"的位置)。
机器人每次向下或向右移动。机器人要到达地图的右下角。(终点,下图中的标记“Finish"的位置)。 可以有多少种不同的路径从起点走到终点?

在这里插入图片描述

分析:

1、状态F(i,j) : 代表到坐标(i,j)有多少不同的路径。
2、状态转移方程:F(i,j) = F(i,j-1) + F(i-1,j)
目的地F(i,j) 根据题目要求只能从F(i-1,j) 左边到它,或者F(i,j-1) 上面到它。 这两种都是不同的路径,因此相加。

考虑下特殊情况,F(0,0), F(0,j), F(i,0)
F(0,0) = 1, 考虑到本身到本身算一条吧, 测试代码中有这条例子。
F(0,j) = 1, 这种路径固定的只能有1条。
F(i,0) = 1, 同样固定。

class Solution {
public:
    /**
     * 
     * @param m int整型 
     * @param n int整型 
     * @return int整型
     */
    int uniquePaths(int m, int n) {
        // write code here
        
        //开辟辅助空间, 一维不够用, 我们开辟二维的。
        //m行,n列的二维数组
        vector<vector<int>> vec;
        vec.resize(m);
        for(auto& it : vec) {
            it.resize(n);
        }
        
        //初始化F(0,0)=1, F(0,j)=1, F(i,0)=1;
        vec[0][0] = 1;
        for(int i = 1; i < n; i++) {
            vec[0][i] = 1;
        }
        for(int i = 1; i < m; i++) {
            vec[i][0] = 1;
        }
        
        for(int i = 1; i < m; i++) {
        	//F(i,j) = F(i,j-1) + F(i-1,j)
            for(int j = 1; j < n; j++) {
                vec[i][j] = vec[i][j - 1] + vec[i - 1][j];
            }
        }
        
        return vec[m-1][n-1];
    }
};

三、矩阵的最小路径和

题目链接

给定一个 n * m 的矩阵 a,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,输出所有的路径中最小的路径和。

在这里插入图片描述
这道题和第二道题很相似。
分析:
1、F(i,j) : 代表到坐标(i,j)最小路径和。
2、F(i,j) = min(F(i,j-1), F(i-1,j)) + v[i,j]当前坐标的值
我们可以看出目的地F(i,j) 可以从F(i,j-1)左边到它, 也可以从F(i-1,j)上边到它。
我们需要取两者最小值 + 加上当前坐标的值。

特殊情况:
F(0,0) = v(0,0);
F(0,j) = F(0,j-1) + v(0,j);
F(i,0) = F(i-1,0) + v(i,0)

#include <iostream>
#include <vector>

using namespace std;

int main() {
    
    int m,n;
    
    while(cin >> m >> n) {
        vector<vector<int>> vec;
        vec.resize(m);
        //二维数组
        for(auto& vit : vec) {
            vit.resize(n);
            for(auto& vvit : vit) {
                cin >> vvit;
            }
        }
        //初始化
        for(int i = 1; i < n; i++) {
            vec[0][i] = vec[0][i - 1] + vec[0][i];
        }
        for(int i = 1; i < m; i++) {
            vec[i][0] = vec[i - 1][0] + vec[i][0];
        }
        
        for(int i = 1; i < m; i++) {
            for(int j = 1; j < n; j++) {
                vec[i][j] = min(vec[i][j - 1], vec[i - 1][j]) + vec[i][j];
            }
        }
        
        cout << vec[m - 1][n - 1] << endl;
    } 
    
    return 0;
}

四、背包问题

题目链接

有N件物品和一个容量为V的背包。第i件物品的价值是C[i],重量是W[i]。求解将哪些物品装入背包可使价值总和最大。

分析:
1、状态F(i,j) ; 代表第i件物品,容量为j总价值最大。
2、状态转移方程:
F(i,j) = F(i-1, j) 代表着第i件物品的大小 > 总背包容量, 放不下情况
F(i,j) = max(F(i-1,j), F(i-1, j-v[i])) 放得下情况。

放得下也分为两种情况:
1、背包剩余的空间能够存放得下, 那就直接加第i件物品, 价值上升。
2、剩余空间放不下, 需要替换掉背包中的某些物品, 这个时候价值可能会上升,也有可能会下降。下降的话还不如不放。

我们用F(i-1,j-v[i]) 代表放得下的所有情况,把第i件物品放下, 总容量j-第i件物品的大小, 提前把第i件物品放到背包里。

因此最后取不放, 放下的F(i-1,j), F(i-1,j-v[i]) 两者的最大值。

#include <iostream>
#include <vector>

using namespace std;

int main() {
    
    int N, V;
    
    while(cin >> N >> V) {
        vector<vector<int>> vec;
        vec.resize(N);
        
        for(auto& vit : vec) {
            vit.resize(2);
            cin >> vit[0] >> vit[1];
        }
        
        //N代表多少件物品
        //V代表重量
        
        vector<vector<int>> flag;
        flag.resize(N + 1);
        
        for(auto& vit: flag) {
            vit.resize(V + 1);
        }
        
        for(int i = 0; i <= N; i++) {
            flag[i][0] = 0;
        }
        for(int i = 0; i <= V; i++) {
            flag[0][i] = 0;
        }
        
        for(int i = 1; i <= N; i++) {
            for(int j = 1; j <= V; j++) {
                //j代表当前容量的大小
                //i代表物品件
                if(vec[i-1][1] > j) {
                    //代表放不下了。
                    flag[i][j] = flag[i-1][j];
                }
                else {
                    //能够放得下
                    flag[i][j] = max(flag[i-1][j], flag[i-1][j-vec[i-1][1]] + vec[i-1][0]);
                }
            }
        }
        
        cout << flag[N][V] << endl;
    }
    
    return 0;
}

五、回文最少分割

题目链接

给定一个字符串,返回把str全部切割成回文串的最少切割数。

在这里插入图片描述
分析:
1、状态F(i) ; 代表着[0,i]的字符串最小切割次数
2、状态转移方程F[i] = min{F(i), F[j]+F(j,i)},(0 < j < i)
如果F(j,i)为回文, F(j) + 1
如果F(j,i)不为回文, 则忽略。

如果F(i)整体为回文的话, F(i) = 0;
如果F(j,i)是回文的话, F(i) = F(j,i) + 1
如果F(j,i)不是回文的话, 则忽略这种分法

举个例子: ABCBAEEE
我们现在求解F(4) 即 ABCB
首先ABCB本身不是回文, 因此F(4) != 0;
根据状态转移方程推:
j = 1, F(1,4) 即 BCB是回文, F(1) + 1;
j = 2, F(2,4) 即CD不会回文, 忽略
j = 3, F(3,4) 即B是回文, F(3) + 1;

最后取最小值。

#include <iostream>
#include <string>
#include <vector>

using namespace std;

bool IsHuiwen(const string& str) {
    if(str.size() == 0) {
        return true;
    }
    
    int begin = 0;
    int end = str.size() - 1;
    while(begin < end) {
        if(str[begin] != str[end]) {
            return false;
        }
        begin++;
        end--;
    }
    return true;
}

int main() {
    
    string str;
    
    while(cin >> str) {
        vector<int> vec;
        vec.resize(str.size() + 1);
        //F(i) 最大分割为i-1
        for(int i = 0; i <= str.size(); i++) {
            vec[i] = i - 1;  
        }
        
        for(int i = 1; i <= str.size(); i++) {
            if(IsHuiwen(str.substr(0, i))){
                //主串为回文
                vec[i] = 0;
            }
            else {
                //不是回文
                for(int j = i - 1; j > 0; j--) {
                    if(IsHuiwen(str.substr(j, i-j))) {
                        if(vec[j] + 1 < vec[i]) {
                            vec[i] = vec[j] + 1;
                        }
                    }                  
                }
            }
        }
        cout << vec[str.size()] << endl;
    }    
    return 0;
}

六、edit-distance

题目链接

给定两个单词word1和word2,请计算将word1转换为word2至少需要多少步操作。
你可以对一个单词执行以下3种操作:
a)在单词中插入一个字符
b)删除单词中的一个字符
c)替换单词中的一个字符

在这里插入图片描述
分析:
1、状态F(i,j) ; 代表word1第[0,i]字符串 转化为word2第[0,j]字符串最小操作步数。
2、状态转移方程:
在这里插入图片描述

F(i-1,j) + 1 的意思我用word1[0,i-1]字符串 已经转化为word2[0,j]字符串, 当添加一个i字符时, 我只需要删掉wrod1[i]字符就可以了, 一步删除操作,所以加1。

F(i, j-1) + 1 的意思我用word1[0,i] 已经转化为word2[0,j-1]了, 当添加一个word2[j]字符时,我们只需要word1字符串中添加一个word2[j]字符就可以了, 一步插入操作, 加1。

当word1[i] == word[j]时, 为F(i-1, j-1)
如果不想当, F(i-1,j-1) + 1, 替换相同就可以了, 一步替换操作, 加1。

因此最后取最小值min。

class Solution {
public:
    /**
     * 
     * @param word1 string字符串 
     * @param word2 string字符串 
     * @return int整型
     */
    int minDistance(string word1, string word2) {
        // write code here
        vector<vector<int>> vec;
        vec.resize(word1.size() + 1);
        
        for(auto& it : vec) {
            it.resize(word2.size() + 1);
        }
        
        vec[0][0] = 0;
        for(int i = 1; i <= word2.size(); i++) {
            vec[0][i] = i; 
        }
        for(int i = 1; i <= word1.size(); i++) {
            vec[i][0] = i;
        }
        
        for(int i = 1; i <= word1.size(); i++) {
            for(int j = 1; j <= word2.size(); j++) {
                vec[i][j] = vec[i][j-1] + 1;
                if(vec[i-1][j] + 1 < vec[i][j]) {
                    vec[i][j] = vec[i-1][j] + 1;
                }
                if(word1[i-1] == word2[j-1]) {
                    //相同 + 0
                    if(vec[i-1][j-1] < vec[i][j]) {
                        vec[i][j] = vec[i-1][j-1];
                    }
                }
                else {
                    //不同, + 1
                    if(vec[i-1][j-1] + 1 < vec[i][j]) {
                        vec[i][j] = vec[i-1][j-1] + 1;
                    }
                }
            }
        }        
        return vec[word1.size()][word2.size()];
    }
};

七、distinct-subsequences

题目链接

给定两个字符串S和T,返回S子序列等于T的不同子序列个数有多少个?
字符串的子序列是由原来的字符串删除一些字符(也可以不删除)在不改变相对位置的情况下的剩余字符(例如,"ACE"is a subsequence of"ABCDE"但是"AEC"不是)
例如:
S =“rabbbit”, T =“rabbit”
返回3

在这里插入图片描述
分析:

1、状态F(i,j) ; S中[0,i]字符串 转化为 T中[0,j]字符串 序列个数。
2、状态转移方程F(i,j) =
F(i-1,j) ; 如果S[i] != T[i]字符的话,
F(i-1, j-1) + F(i-1, j) ; 如果S[i] == T[i]的话。

如果S[i] != T[i]字符的话, 我们不管怎样划分,S[i]最后一个字符是不用上的。用S[0,i-1]之前的字符取划分T[0,i]
因此 F(i,j) = F(i-1,j)

例如: S: “rabz” T:“rab”
F(4,3) = F(3,4)
因此S中的’z’ 不等于 T中的’b’
我们就用"rab" 去划分 ”rab" , 把’z’字符删除掉。
“r" “ab” ; “ra”, “b” 这两种划分法。

如果S[i] = T[i]的话, 你可以使用S[i],他们是匹配的, 然后考虑F(i-1,j-1)多少种。
你也可以不使用S[i], 那就S[0,i-1] 转化。 用它和不用它是不同的划分, 因此相加。

例如: S: “rabb1” T:“rab”
最后两个字符相同,
如果使用S[i] 即 b1和b对应, “r” “ab1”, “ra”, “b1”
不使用S[i] , “r” “ab” , “ra”, “b”

class Solution {
public:
    /**
     * 
     * @param S string字符串 
     * @param T string字符串 
     * @return int整型
     */
    int numDistinct(string S, string T) {
        // write code here
        
        vector<vector<int>> vec;
        vec.resize(S.size() + 1);
        
        for(auto& it : vec) {
            it.resize(T.size() + 1);
        }
        
        vec[0][0] = 1;
        for(int i = 1; i <= T.size(); i++) {
            vec[0][i] = 0;
        }
        for(int i = 1; i <= S.size(); i++) {
            vec[i][0] = 1;
        }
        
        for(int i = 1; i <= S.size(); i++) {
            for(int j = 1; j <= T.size(); j++) {
                if(S[i-1] == T[j-1]) {
                    //相同 f(i,j) = f(i-1,j-1) + f(i-1, j);
                    vec[i][j] = vec[i-1][j-1] + vec[i-1][j];
                }
                else {
                    //不同 f(i,j) = f(i-1, j);
                    vec[i][j] = vec[i-1][j];
                }
            }
        }
        
        return vec[S.size()][T.size()];
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值