写在前面:
一个月以来学习了dp,bfs,dfs等算法,重点做了dp的题目,虽然现在还不能做全部的题目,但是很多题目也会有意识的去使用动态规划的知识去解答,接下来我准备好好将自己做过的笔记整理一下,勤记录笔记然后将笔记整理下来形成自己的解题和复习的方法(也帮助dp菜鸟少踩一些坑),以提升自己的算法能力(同时不能忘记数据结构的学习)。
/*
* 本文动态更新。
* 欢迎提出任何建议或者想法:17816110021
* 也欢迎大佬提出指点
* 左手代码右手诗
*/
动态规划篇:
解决两类问题:
1 优化。例如最短路径,最少钱数
2 计数
通常可以使用DP解决的问题:
1 斐波那契数列
2 最长公共子序列
3 背包问题
4 两点之间的最短路径
5 单元的最短路径
我觉得在做题的时候首先
1.要确定什么时候可以确定使用动态规划
2.一旦确定了使用动态规划之后又该如何去确定状态表达式(dp[i][j])
3.确定了状态表达式之后就要学会分析状态之间如何转化,动态规划本质上类同递归,但是比递归多了一个查表的过程,因此这个建表的过程或者推导的过程应该去查看YouTube上的花花酱的视频以及图解算法中的动态规划章节的内容,先看完图解算法中的内容然后再看花花酱的视频(案例推导),这样可以帮助我们更快的完成动态规划的状态的转移。
4.优化子结构,一般当前状态可以由上一状态转化,也可以有其他操作(比如下题中删除,修改等操作带来了不同的状态),优化的目的就是从这些子状态中选择最符合题意的状态。
动态规划的分类:
根据状态表达式转化过程的分类我认为动态规划可以分成下面这几类:
1.直接从上一状态向下一状态转化
2.上一状态+当前的状态的一些操作处理。
3.上一状态+当前选中量一些操作处理,上一状态和其他状态,以及对当前选中量操作处理。(例如dp[i][j]是当前状态,dp[i-1][j]是上一状态,dp[i-1][k](k>j)就是其他状态)
从我做过的题目中大致可以分成这么几类。
例题:
题目:
给定两个字符串s1和s2,两者的编辑距离定义为将s1转换为s2的最小编辑操作数(等价于将s2转换为s1的最小编辑操作数)。
编辑操作有3种:插入一个字符、删除一个字符、替换一个字符。
例如:cat和cbt的编辑距离是1(将a替换为b);cat到ca的编辑距离是1(删除t);ct到cat的编辑距离是1(插入a);xcat到caty的编辑距离是2(删除x,插入y)。
知道了编辑距离的定义,那么如何求最小编辑距离呢?这里用到了动态规划的思想。(本题是多阶段决策的过程)
用例子来说明,假如我们要求解 jary和jerry的最小编辑距离,那么首先要创建如下矩阵:
j | a | r | y | ||
0 | 1 | 2 | 3 | 4 | |
j | 1 | ||||
e | 2 | ||||
r | 3 | ||||
r | 4 | ||||
y | 5 |
这个矩阵什么意思呢?第一行是字符串jary,第一列是字符串jerry,每个标有数字的单元格代表了两个字符串的子串的最小编辑距离。第二行第二列的0代表两个字符串都取空子串的时候,编辑距离就是0(子串相等);第二行第三列的1代表当jerry的子串取空,jary的子串取j时,两个子串的最小编辑距离是1(给jerry的子串插入j)。其他的依次类推,可以很容易得出当前矩阵中的第二行和第二列的数字。
而我们最终要求的两个字符串的最小编辑距离对应的就是矩阵右下角的那个单元格,它代表当jary子串取jary,jerry子串取jerry时,两个子串的最小编辑距离,也就是两个字符串的最小编辑距离。
这里我先直接说怎么求,然后再解释原理。看下面的矩阵,我在中心空白的位置标上了从x1到x20,这里x后面的数字代表我们求解时的顺序。
j | a | r | y | ||
0 | 1 | 2 | 3 | 4 | |
j | 1 | x1 | x6 | x11 | x16 |
e | 2 | x2 | x7 | x12 | x17 |
r | 3 | x3 | x8 | x13 | x18 |
r | 4 | x4 | x9 | x14 | x19 |
y | 5 | x5 | x10 | x15 | x20 |
如果按顺序求解的话,那么在求解每一个值的时候,它的左、上、左上三个位置的单元格值肯定都是已知的,将这三个单元格里的值分别定义为left、top、leftTop,则要求解的单元格的值v为:
cost=若单元格横向对应的字符和纵向对应的字符相等则为0否则为1
min(left+1,top+1,leftTop+cost)
按照求解方法求解后的矩阵:
j | a | r | y | ||
0 | 1 | 2 | 3 | 4 | |
j | 1 | 0 | 1 | 2 | 3 |
e | 2 | 1 | 1 | 2 | 3 |
r | 3 | 2 | 2 | 1 | 2 |
r | 4 | 3 | 3 | 2 | 2 |
y | 5 | 4 | 4 | 3 | 2 |
取右下角的值,因此jary和jerry的编辑距离是2(替换a为e,插入一个r)。
求解原理
通过上面的介绍我们可以用矩阵求两个字符串的最小编辑距离了,但是这么做的原理是什么呢?其实很简单,当我们要求字符串s[1...i]到t[1...j]的编辑距离的时候:
- 如果我们知道可以在k1个操作内将s[1...i-1]转换为t[1...j],那么用k1+1次操作一定能将s[1...i]转化为t[1...j],因为只需要先做一次移除操作移除s[i]将s[1...i]转化为s[1...i-1],然后再做k1个操作就可以转换为t[1...j]。
- 如果我们知道可以在k2个操作内将s[1...i]转换为t[1...j-1],那么用k2+1次操作一定能将s[1...i]转化为t[1...j],因为我们可以先用k2次操作将s[1...i]转化为t[1...j-1],然后再执行一次插入操作在末尾插入t[j]即可将s[1...i]转化为t[1...j]
- 如果我们知道可以在k3个操作内将s[1...i-1]转化为t[1...j-1],那么如果s[i]==t[j],则将s[1...i]转换为t[1...j]只需要k3次操作,如果s[i]!=t[j],则需要做一次替换操作将s[i]替换为t[j],这种情况下需要k3+1次操作。
而上面我们讨论的3中情况下的k1、k2、k3就对应着矩阵里一个单元格的左、上、左上的单元格里的值。
上述结论可以表述为如下公式:
实现代码
明白了原理之后,写代码很简单,就是用代码模拟计算矩阵的过程(java实现),时间复杂度是O(|s1|*|s2|) (|s1|、|s2|分别代表字符串s1和s2的长度):
package common;
import org.junit.Assert;
public class LevenshteinDistance {
public static int getDistance(String src, String des) {
int[][] m=new int[des.length()+1][];
for (int i = 0; i < m.length; i++) {
m[i]=new int[src.length()+1];
}
for(int i=0;i
DP与字符串结合
dp与字符串相结合其实还是子序列的一种,只不过作为字符串通常有他的特殊性。在此类题中常见常见的就是回文。回文的检测就要占用O(n)的时间,所以尽可能的讲回文的检测与字串的搜索合并到一起这样就可以降低他的时间复杂度。状态表达式通常是dp[i][j],i代表以第i个字符结尾,以第j个字符串开始的字串,然后对其进行状态更行。这种题也给我们带来一个新的角度,有可能一步没法办做到状态的转化,可以考虑提前处理好一些数据,比如本题,我先找到dp[i][j]所以的字符串是否为回文,然后将这一个个的字符串检索过去并进行状态更新,就可以找到字符串中所有的回文串的个数了,并且前面有些状态计算过了后面就不用重新计算。
例题:
题目:计算一串字符串中的有多少回文串。
思路很清晰:
1.先将每一个字符所对应的字串是回文先进行状态更新(True,False)
2.再所有这些字串,并进行状态转移(这里假设你知道如何状态转移)。
public int countSubstrings(String s) {
if(s.equals("")){
return 0;
}
int count=0;
char[] cs=s.toCharArray();
int n=cs.length;
boolean DP[][]=new boolean[n][n];
for(int i=0;i=j){
return true;
}
else{
return DP[i][j];
}
}
leetcode 730 计算不同的回文子序列
int len = S.length();
if (S == null || S.length() == 0)
return 0;
int mod = 1000000007, l, r;
char [] chars = S.toCharArray();
int dp[][] = new int[chars.length][chars.length];
for (int i = chars.length;i > -1;i--)
{
dp[i][i] = 1;
for (int j = i + 1;j < chars.length;j++)
{
//当两边不一样的时候使用的两边的计算方式
//原因是dp[i][j] 代表的是字符串从j到i中没有重复的回文子串
//所以dp[i + 1][j] 与dp[i][j - 1]其中重复计算了dp[i + 1][j - 1]的内容
//故而需要将其中重复的量去除掉
if (chars[i] != chars[j])
{
dp[i][j] = (dp[i][j - 1] + dp[i + 1][j] - dp[i + 1][j - 1])%mod;
}
else
{
//当两侧是相同的时候需要去排除里面中重复的项目
//但是首先总数是等于dp[i][j] = 2*dp[i + 1][j - 1];
//接下来开始计算其中因为相同字符造成的重复项
dp[i][j] = (2*dp[i + 1][j - 1]) % mod;
l = i + 1;
r = j - 1;
//利用两个方向指针指向其中有多少对相同的字符串
//重复
while (1 <= r && chars[i] != chars[l])
{
l++;
}
while (l <= r && chars[i] == chars[l])
{
r--;
}
if (l > r)
dp[i][j] = (2 + dp[i][j]) % mod;
else if (1 == r)
dp[i][j] = (1 + dp[i][j]) % mod;
else if (r - l >= 2)
dp[i][j] = (dp[i][j] - dp[l + 1][r - 1]);
}
dp[i][j] = (dp[i][j] + mod) % mod;
}
}
return dp[0][chars.length - 1];
利用数据结构可以降低代码的复杂度
例题:
序列X_1, X_2, ..., X_n
是斐波那契状,如果:
n >= 3
X_i + X_{i+1} = X_{i+2}
对全部i + 2 <= n
给定一个严格增加A
的正整数 阵列 形成一个序列,找到最长的斐波那契样子序列的长度A
。如果不存在,则返回0。
(回想一下,子序列是A
通过删除任意数量的元素(包括无)来从另一个序列中派生出来的A
,而不改变其余元素的顺序。例如,[3, 5, 8]
是子序列[3, 4, 5, 6, 7, 8]
。)
思路:
想法很明晰就是利用O(N*3)的复杂度对整串数字进行搜索,A[i] =A[k] + A[j];
然后利用哈希表来查找A[j],算出对应的索引j;利用数组存储。dp[i][j] = dp[map.get(A[j])][k] + 1;
Code:
//LC873
class Solution {
public int lenLongestFibSubseq(int[] A) {
int n = A.length;
int [][] dp = new int[n][n];
Map map = new HashMap<>();
for (int i = 0;i < n;i++)
{
if (!map.containsKey(A[i]))
{
map.put(A[i],i);
for(int j=0;j= 0;j--)
{
int cur = A[i] - A[j];
if (cur >= A[j])
{
break;
}
if (map.containsKey(cur))
{
dp[j][i] = dp[map.get(cur)][j] + 1;
maxLen = Math.max(maxLen,dp[j][i]);
}
}
}
return maxLen;
}
}
dp中的经典背包问题
我认为这种题表面上还是在原序列中找子序列,并且让子序列符合新的规则,但是实际上这里面的规则已经完全变了。当然在LeetCode的做题中发现了一些问题。
问题1: 状态转移方程会列写,但是coding的时候发生问题。
问题2: 初始化,结束条件(即所谓的边界条件的判断)有问题。
问题3: 列表的长度存在问题。
问题4:状态表达式的确定,有一些题目不能直接表达,要学会预先处理或者间接表达。
硬币找零问题:
您将获得不同面额的硬币和总金额。编写一个函数来计算构成该数量所需的最少数量的硬币。如果这笔钱不能由任何硬币组合弥补,则退货-1。
思路:就是第i个硬币选还是不选的问题,因为硬币的数量是不限定的,所以这是一个完全背包的问题。所以状态转移方程很简单:dp[k] = min(dp[k-1],dp[k-coins[i]]+1)
Code:
if (amount == 0)
{
return -1;
}
int len = coins.size();
vectorminCount(amount + 1, amount + 1);
minCount[0] = 0;
for (int i = 0; i < len; i++)
for (int k = i; k <= amount; k++)
{
minCount[k] = min(minCount[k], minCount[k - coins[i]] + 1);
}
return minCount[amount] > amount + 1 ? -1 : minCount[amount];
最新消息:经过这样的训练之后,发现解决LeetCode中的一些简单,中档题不存在问题。所以应该加强这样的练习,同时我发现,高端的动态规划(ACM级)都会对状态进行抽象(或者称之为封装),然后进行状态转移,这类题目通常状态的抽象是很难的,而状态的转移的决策反而很简单。
DFS+DP的题目
例题:LeetCode140
题目:
给定一个非空字符串s和一个包含非空单词列表的字典wordDict,在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中。返回所有这些可能的句子。
说明:
- 分隔时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
Code:
class Solution {
private List wordDict;
public List wordBreak(String s, List wordDict) {
List result = new ArrayList();
if (s.length() == 0 || wordDict.size() == 0) {
return result;
}
this.wordDict = wordDict;
int maxLength = 0;
for (String str : wordDict) {
maxLength = Math.max(maxLength, str.length());
}
boolean[] possible = new boolean[s.length() + 1];
possible[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (int j = i; j > 0 && i - j + 1 <= maxLength; j--) {
if (possible[j - 1] && wordDict.contains(s.substring(j - 1, i))) {
possible[i] = true;
break;
}
}
}
System.out.println(possible[s.length()]);
if (!possible[s.length()]) {
return result;
}
List list = new ArrayList();
helper(s, 0, possible, list, result);
return result;
}
private void helper(String s, int index, boolean[] possible, List list, List result) {
if (index == s.length()) {
StringBuilder sb = new StringBuilder();
for (String str : list) {
sb.append(str);
sb.append(" ");
}
sb.deleteCharAt(sb.length() - 1);
result.add(sb.toString());
return;
}
if (!possible[index]) {
return;
}
for (int i = index; i < s.length(); i++) {
if (!wordDict.contains(s.substring(index, i + 1))) {
continue;
}
list.add(s.substring(index, i + 1));
helper(s, i + 1, possible, list, result);
list.remove(list.size() - 1);
}
}
}
分析:首先利用dp的方法来检查每一种单词的分割的方法是否是合法的(即划分的每个单词都在字典中),然后用DFS的方法来枚举每一种符合要求的分法。
DFS:
回溯也是DFS的一种,回溯通常用来解决的问题是求解排列组合问题。而普通DFS则是用来求解可达性问题,换言之回溯的意义是找出不止一条可达性路径。
因为Backtracking不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。
例题:
给定包含来自包含数字的字符串2-9,返回该数字可能表示的所有可能的字母组合。
下面给出了数字到字母的映射(就像在电话按钮上一样)。请注意,1不会映射到任何字母。(LC-17)
Code:
void combinations(string digits, vectornums, vector&ans, string temp, int start)
{
if (start >= digits.size())
{
ans.push_back(temp);
return;
}
int index = digits[start] - '0';
for (int i = 0; i < nums[index].size(); i++)
{
combinations(digits, nums, ans, temp + nums[index][i], start + 1);
}
return;
}
vectorletterCombinations(string digits)
{
vector nums = { "","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz" };
vectorans;
if (digits.size() == 0)
{
return ans;
}
combinations(digits, nums, ans, "", 0);
return ans;
}
这种题目一般都是模板题,按照模板套路好好准备就可以。
BFS:
例题:
给定两个单词(beginWord和endWord)和一个字典的单词列表,找到从beginWord到endWord的最短变换序列的长度,这样:
一次只能更改一个字母。
每个转换后的单词必须存在于单词列表中。需要注意的是beginWord是不是变换词。
注意:
如果没有这样的转换序列,则返回0。
所有单词都有相同的长度。
所有单词仅包含小写字母字符。
您可以假设单词列表中没有重复项。
您可以假设beginWord和endWord是非空的并且不相同。
例1:
输入:
beginWord =“hit”,
endWord =“cog”,
wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
思路:
这种问题其实也会容易想到就是直接利用DFS,将字典的单词列表看作表格。
Code:
int ladderLength(string beginWord, string endWord, vector& wordList)
{
unordered_set dict(wordList.begin(), wordList.end());
if (!dict.count(endWord)) { return 0; }
queueq;
q.push(beginWord);
int l = beginWord.length();
//record the depth
int step = 0;
while (!q.empty())
{
++step;
for (int size = q.size();size>0;size--)
{
string w = q.front();
q.pop();
for (int i = 0;i < l;i++)
{
//record pos which was exchanged
char ch = w[i];
for (int j = 'a';j<='z';j++)
{
w[j] = j;
if (w == endWord) { return step++; }
if (!dict.count(w)) { continue; }
dict.erase(w);
q.push(w);
}
w[i] = ch;
}
}
}
return 0;
}
例题:
给定一个grid0和1的非空二维数组,一个岛是一组1(代表陆地)4方向连接(水平或垂直)。您可以假设网格的所有四个边都被水包围。
找到给定2D阵列中岛的最大面积。(如果没有岛,则最大面积为0.)
例1:
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
鉴于以上网格,返回6。注意答案不是11,因为岛必须是4向连接。
思路:
这种求解最大连通问题一般来讲可以直接考虑DFS
Code:
#include "stdafx.h"
#include
#include
#include
using namespace std;
int max(int a, int b)
{
return a > b ? a : b;
}
int maxAreaOfIsland(vector>& grid)
{
int area = 0;
int temp = 0;
for (int i = 0; i < grid.size(); i++)
{
for (int j = 0; j < grid[0].size(); j++)
{
if (grid[i][j] == 1)
{
temp = dfs(grid, i, j);
area = max(area, temp);
}
}
}
return area;
}
int dfs(vector>&grid, int i, int j)
{
if (i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size() || grid[i][j] == 0)
{
return 0;
}
int tempMaxArea = 1; //这里值必须是1,因为是在grid[i][j]的情况下开始搜索,所以初始值必定为1
//if the point has been visited,set the mark
grid[i][j] = 0;
tempMaxArea += dfs(grid, i, j - 1) + dfs(grid, i - 1, j) + dfs(grid, i, j + 1) + dfs(grid, i + 1, j);
return tempMaxArea;
}