动态规划 框架
确定状态:原问题和子问题中变化的变量。状态决定了问题什么时候算是解决了。
确定dp函数定义:
一般就是问题
也可以是中间子问题,比如第二、三个例题
确定选择并择优:对于当前状态,有哪些选择可以改变状态,向着问题的解决前进。
明确badcase
使用备忘录数组记录已经访问过的状态
例题 最长公共子序列 题目:#1143
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,”ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
思路:
用两个指针,从指针位置到字符串末尾,是当前状态下的字符串。状态量其实就是字符串长度。
dp函数:返回当前长度下的两个字符串的最长公共子序列长度
选择并择优:如果当前两个指针的字符相等,则返回递归函数值+1;如果不等,有三种可能:
badcase是任意一个指针到字符串末尾后了,则一个字符串变为空,返回0
最后使用备忘录数组记录已经计算过的情况值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.util.*;class Solution { public int longestCommonSubsequence (String text1, String text2) { int [][] dptable=new int [text1.length()][text2.length()]; for (int [] row:dptable){ Arrays.fill(row,-1 ); } return dp(text1,0 ,text2,0 ,dptable); } public int dp (String s1,int idx1,String s2,int idx2,int [][]dptable) { if (idx1==s1.length()||idx2==s2.length())return 0 ; if (dptable[idx1][idx2]!=-1 )return dptable[idx1][idx2]; if (s1.charAt(idx1)==s2.charAt(idx2)){ dptable[idx1][idx2]=1 +dp(s1,idx1+1 ,s2,idx2+1 ,dptable); } else { dptable[idx1][idx2]=Math.max( dp(s1,idx1+1 ,s2,idx2,dptable), dp(s1,idx1,s2,idx2+1 ,dptable) ); } return dptable[idx1][idx2]; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution { public int longestCommonSubsequence (String text1, String text2) { int m = text1.length(), n = text2.length(); int [][] dp = new int [m + 1 ][n + 1 ]; for (int i = 0 ; i < m; i++) { for (int j = 0 ; j < n; j++) { char c1 = text1.charAt(i), c2 = text2.charAt(j); if (c1 == c2) { dp[i + 1 ][j + 1 ] = dp[i][j] + 1 ; } else { dp[i + 1 ][j + 1 ] = Math.max(dp[i + 1 ][j], dp[i][j + 1 ]); } } } return dp[m][n]; } }
数组中等差递增子区间的个数 题目:#413
数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P<Q<N 。如果满足以下条件,则称子数组(P, Q)为等差数组:元素 A[P], A[p + 1], …, A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。函数要返回数组 A 中所有为等差数组的子数组个数。
这里的dp函数就是一个子问题: dp[i] 表示以 A[i] 为结尾 的等差递增子区间的个数。最后求完以后只要取dp数组的最大值就可以解决原问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public int numberOfArithmeticSlices(int[] nums) { int n=nums.length; if(n<3)return 0; int[]dp=new int[n]; for(int i=2;i<n;i++){ if(nums[i]-nums[i-1]==nums[i-1]-nums[i-2]){ dp[i]=dp[i-1]+1; } } int sum=0; for(int v:dp)sum+=v; return sum; }
最长递增子序列 题目:#300
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
dp[i]是以当前数结尾的最长递增子序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public int lengthOfLIS (int [] nums) { int n=nums.length; if (n==0 )return 0 ; int []dp=new int [n]; for (int i=0 ;i<n;i++){ int maxnow=1 ; for (int j=0 ;j<i;j++){ if (nums[j]<nums[i]){ maxnow=Math.max(maxnow,dp[j]+1 ); } } dp[i]=maxnow; } Arrays.sort(dp); return dp[n-1 ]; }
滑动窗口 主要解决字符串子串匹配问题
框架 维护一个窗口,不断滑动,然后更新答案
1 2 3 4 5 6 7 8 9 10 11 12 13 int left = 0 , right = 0 ;while (right < s.size()) { window.add(s[right]); right++; while (window needs shrink) { window.remove(s[left]); left++; } }
一般用HashMap实现窗口,用到getOrDefault(key,defaultvalue)
函数来获取值,同时避免key不存在时得到null。
例题 最小覆盖子串 题目:#76
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。
1 2 输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import java.util.*;class Solution { public String minWindow (String s, String t) { HashMap<Character,Integer> need=new HashMap<>(); HashMap<Character,Integer> window=new HashMap<>(); int start=0 ,end=0 ,len=s.length()+1 ; int fstart=0 ,fend=0 ; boolean flag=false ,flag2=false ; String re="" ; for (int i=0 ;i<t.length();i++){ need.put(t.charAt(i),need.getOrDefault(t.charAt(i),0 )+1 ); } while (end<s.length()){ flag=false ; while (!flag2map(need,window)&&end<s.length()){ window.put(s.charAt(end),window.getOrDefault(s.charAt(end),0 )+1 ); end++; } while (flag2map(need,window)){ if (need.containsKey(s.charAt(start))){ window.put(s.charAt(start),window.get(s.charAt(start))-1 ); } start++; flag=true ; flag2=true ; } if (flag&&(end-start+1 <len)){ len=end-start+1 ; fstart=start-1 ; fend=end-1 ; } } if (fend==fstart&&!flag2){ return re; } else { return s.substring(fstart,fend+1 ); } } boolean flag2map (HashMap<Character,Integer> need,HashMap<Character,Integer> window) { for (Character tmp: need.keySet()){ Integer win=window.get(tmp); if (win==null )return false ; if (need.get(tmp).intValue()>win.intValue())return false ; } return true ; } }
字符串排列 题目#567
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例1:
输入: s1 = “ab” s2 = “eidbaooo” 输出: True 解释: s2 包含 s1 的排列之一 (“ba”).
示例2:
输入: s1= “ab” s2 = “eidboaoo” 输出: False
思路:维护一个s1长度的窗口,不是排列就right+1,left+1,即向右平移窗口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution { public boolean checkInclusion (String s1, String s2) { HashMap<Character,Integer> need=new HashMap<>(); HashMap<Character,Integer> window=new HashMap<>(); for (int i=0 ;i<s1.length();i++){ need.put(s1.charAt(i),need.getOrDefault(s1.charAt(i),0 )+1 ); } int left=0 ,right=s1.length()-1 ; if (right>=s2.length())return false ; for (int i=0 ;i<=right;i++){ window.put(s2.charAt(i),window.getOrDefault(s2.charAt(i),0 )+1 ); } while (true ){ if (check(need,window))return true ; left++; right++; if (right>=s2.length())break ; window.put(s2.charAt(right),window.getOrDefault(s2.charAt(right),0 )+1 ); window.put(s2.charAt(left-1 ),window.getOrDefault(s2.charAt(left-1 ),0 )-1 ); } return false ; } boolean check (HashMap<Character,Integer> need,HashMap<Character,Integer> window) { for (Character tmp: need.keySet()){ if (need.get(tmp).intValue()!=window.getOrDefault(tmp,-1 ).intValue())return false ; } return true ; } }
找到字符串中所有字母异位词 题目:#438
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
字母异位词指字母相同,但排列不同的字符串。 不考虑答案输出的顺序。 示例 1:
输入: s: “cbaebabacd” p: “abc”
输出: [0, 6]
解释: 起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。 起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。
思路和上面一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution { public List<Integer> findAnagrams (String s, String p) { HashMap<Character,Integer> need=new HashMap<>(); HashMap<Character,Integer> window=new HashMap<>(); List<Integer> re=new LinkedList<>(); for (int i=0 ;i<p.length();i++){ need.put(p.charAt(i),need.getOrDefault(p.charAt(i),0 )+1 ); } int left=0 ,right=p.length()-1 ; if (right>=s.length())return re; for (int i=0 ;i<=right;i++){ window.put(s.charAt(i),window.getOrDefault(s.charAt(i),0 )+1 ); } while (true ){ if (check(need,window))re.add(left); left++; right++; if (right>=s.length())break ; window.put(s.charAt(right),window.getOrDefault(s.charAt(right),0 )+1 ); window.put(s.charAt(left-1 ),window.getOrDefault(s.charAt(left-1 ),0 )-1 ); } return re; } boolean check (HashMap<Character,Integer> need,HashMap<Character,Integer> window) { for (Character tmp: need.keySet()){ if (need.get(tmp).intValue()!=window.getOrDefault(tmp,-1 ).intValue())return false ; } return true ; } }
最长无重复子串 题目:#3
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。 示例 2:
输入: s = “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。 示例 3:
输入: s = “pwwkew” 输出: 3 解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。 请注意,你的答案必须是 子串 的长度,”pwke” 是一个子序列,不是子串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.util.*;class Solution { public int lengthOfLongestSubstring (String s) { HashMap<Character,Integer> window=new HashMap<>(); int left=0 ,right=0 ; int maxlen=0 ; while (right<s.length()){ while (right<s.length()&&(window.getOrDefault(s.charAt(right),0 )==0 )){ window.put(s.charAt(right),1 ); right++; } maxlen=Math.max(maxlen,right-left); if (right==s.length())break ; while (window.getOrDefault(s.charAt(right),0 )!=0 ){ window.put(s.charAt(left),0 ); left++; } } return maxlen; } }
双指针 快慢指针
判断链表是否有环(有环两个指针最终会相遇)
找链表中点(快指针到链表末端,慢指针到中点)
寻找链表倒数第k个数(快指针先走k步,再和慢指针一起同速走)
左右指针
二分查找
有序数组中找两个数之和为目标值(左指针为0,右指针为末尾,两数和小于目标值,left++,大于,right–)
反转数组
滑动窗口
BFS 本质是图中找最短路径
核心数据结构是队列
框架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int BFS (Node start, Node target) { Queue<Node> q; Set<Node> visited; q.offer(start); visited.add(start); int step = 0 ; while (q not empty) { int sz = q.size(); for (int i = 0 ; i < sz; i++) { Node cur = q.poll(); if (cur is target) return step; for (Node x : cur.adj()) if (x not in visited) { q.offer(x); visited.add(x); } } step++; } }
例题 打开转盘锁 题目#752
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,’0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 ‘0000’ ,一个代表四个拨轮的数字的字符串。
列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。
思路:
对于每次数字,都有8个相邻选择(4位,每位+1或-1),每次把这些选择入队
用一个set记录访问过的数字组合
String和char数组的转换:s.toCharArray()
,new String(ch)
int和char转换:(char)(num+'0')
,ch-'0'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class Solution { public int openLock (String[] deadends, String target) { Queue<String> que=new LinkedList<>(); Set<String> visited=new HashSet<>(); for (String dead:deadends){ visited.add(dead); } if (visited.contains("0000" ))return -1 ; que.offer("0000" ); visited.add("0000" ); int size=0 ; int num=0 ; String tmp; while (!que.isEmpty()){ size=que.size(); for (int i=0 ;i<size;i++){ tmp=que.poll(); if (tmp.equals(target))return num; for (int j=0 ;j<4 ;j++){ String tmp2=plusUp(tmp,j); if (!visited.contains(tmp2)){ que.offer(tmp2); visited.add(tmp2); } String tmp3=plusDown(tmp,j); if (!visited.contains(tmp3)){ que.offer(tmp3); visited.add(tmp3); } } } num++; } return -1 ; } String plusUp (String str,int i) { char [] ch=str.toCharArray(); ch[i]=(char )('0' +((ch[i]-'0' )+1 )%10 ); return new String(ch); } String plusDown (String str,int i) { char [] ch=str.toCharArray(); ch[i]=(char )('0' +((ch[i]-'0' )+9 )%10 ); return new String(ch); } }
回溯 适合做排列组合问题,即遍历所有可行解。
框架 1 2 3 4 5 6 7 8 9 10 result = [] def backtrack (路径, 选择列表 ): if 满足结束条件: result.add(路径) return for 选择 in 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择
路径的数据结构 :用Stack 或者Deque ,后者效率更高
1 2 3 4 5 6 7 Deque<Integer> path=new LinkedList<>(); path.addLast(x); dfs(); path.removeLast(); LinkedList<Integer> tmp=new LinkedList<>(path);
选择列表 :
特别注意确定选择列表到底是什么
例如:
子集问题#78,选择列表是当前数是否选,二选一
组合总和问题#39,选择列表是所有可选的数,n选一
去重 :
不讲顺序(1,2,3和2,1,3认为是一样的):使用begin标记选择列表的开始,每次从begin开始往后遍历
讲顺序,用数组
例题 子集 题目 :#78
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
思路 :
不讲顺序,用begin,选择列表是当前数选或者不选
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public List<List<Integer>> subsets(int [] nums) { List<List<Integer>> re=new LinkedList<>(); Deque<Integer> path=new ArrayDeque<>(); dfs(nums,re,path,0 ); return re; } private void dfs (int [] nums,List<List<Integer>> re,Deque<Integer> path,int begin) { if (begin==nums.length){ ArrayList<Integer> tmp=new ArrayList<>(path); re.add(tmp); return ; } dfs(nums,re,path,begin+1 ); path.addLast(nums[begin]); dfs(nums,re,path,begin+1 ); path.removeLast(); } }
组合总和 题目 :#39
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
说明:所有数字(包括 target)都是正整数。解集不能包含重复的组合。
思路 :
不讲顺序,用begin,选择列表是candidates里的所有数,n选1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution { public List<List<Integer>> combinationSum(int [] candidates, int target) { List<List<Integer>> re=new LinkedList<List<Integer>>(); if (candidates.length==0 )return re; Arrays.sort(candidates); Deque<Integer> path=new ArrayDeque<>(); dfs(candidates,target,re,path,0 ); return re; } void dfs (int [] candidates, int target,List<List<Integer>> re,Deque<Integer> path,int begin) { if (target==0 ){ re.add(new ArrayList<>(path)); return ; } for (int i=begin;i<candidates.length;i++){ if (candidates[i]>target){ break ; } else { path.offerLast(candidates[i]); dfs(candidates,target-candidates[i],re,path,i); path.pollLast(); } } } }
单词搜索 题目:#79
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board = [ [‘A’,’B’,’C’,’E’], [‘S’,’F’,’C’,’S’], [‘A’,’D’,’E’,’E’] ]
给定 word = “ABCCED”, 返回 true 给定 word = “SEE”, 返回 true 给定 word = “ABCB”, 返回 false
思路:
这个是讲顺序的,所以要用一个visited数组来记录访问过的数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution { public boolean exist (char [][] board, String word) { int row=board.length; int col=board[0 ].length; boolean [][]visited=new boolean [row][col]; for (int i = 0 ; i <row ; i++) { for (int j = 0 ; j < col; j++) { if (starExist(board,word,i,j,0 ,visited))return true ; } } return false ; } boolean starExist (char [][] board, String word,int i,int j,int idx,boolean [][]visited) { if (i<0 ||i>=board.length||j<0 ||j>=board[0 ].length)return false ; if (visited[i][j])return false ; if (board[i][j]==word.charAt(idx)){ if (idx==word.length()-1 )return true ; visited[i][j]=true ; if (starExist(board,word,i-1 ,j,idx+1 ,visited))return true ; if (starExist(board,word,i+1 ,j,idx+1 ,visited))return true ; if (starExist(board,word,i,j-1 ,idx+1 ,visited))return true ; if (starExist(board,word,i,j+1 ,idx+1 ,visited))return true ; visited[i][j]=false ; return false ; } else { return false ; } } }
并查集 并查集是一种树型的数据结构,用于处理一些不交集的合并及查询问题。它可以支持两种操作
(1) 将两个不相交的集合合并
(2) 判断两个元素是否在一个集合中
两种操作的时间复杂度近乎O ( 1 )
框架 用一个数组p来存储每个节点的父节点的编号,即 p [ x ] 表示编号为 x 的结点的父节点的编号为 p [ x ]
初始化:把每个点所在集合初始化为自己,即p [ x ] = x
查找:
1 2 3 4 int find (int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; }
合并:
1 2 3 void union (int [] p, int a, int b) { p[find(a)] = find(b); }
例题 无向图中连通分量的数目 题目:323
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution { int [] parent; int find (int parent[], int i) { if (parent[i] == -1 ) return i; return find(parent, parent[i]); } void union (int parent[], int x, int y) { int xset = find(parent, x); int yset = find(parent, y); if (xset != yset) parent[xset] = yset; } public int countComponents (int n, int [][] edges) { int len = edges.length; parent = new int [n]; Arrays.fill(parent, -1 ); for (int i = 0 ; i < len; i++) { union(parent, edges[i][0 ], edges[i][1 ]); } int count = 0 ; for (int i = 0 ; i < n; i++) if (parent[i] == -1 ) count++; return count; } }