动态规划
动态规划
思想
缓存迭代
思想。
应用前提
- 问题具有
最优子结构
:子问题最优解->整个问题最优解,大的问题拆解为类似的子问题。 无后效性
:前一个状态推导出下一个状态,后面状态不影响前面的状态,可以只关注当下与前置推演即可。重复子问题
:有子问题重叠计算的情况。由于有子问题重叠计算的情况,所以递归过程浪费了时间。而动态规划将中间结果
保存在数组中,以空间换时间,保证每个子问题只求解一次,提升了效能。也正是缓存迭代
思想的体现。
实践
139.Word Break
一个字符串S,一个单词字典wordDict。例如:s=“leetCode”,wordDict={“leet”,“code”}。问:s整体能否刚好被拆分为wordDict中的字符子串的组合。其中,wordDict中字符子串本身无重复,但是组合过程中每个字符子串可以被重复使用。比如“code”可以用两次去组合出“codecode”。
方法一:递归解法。
空间复杂度 树高 n,O(n)。
时间复杂度 每个s有n+1种分法,要么分,要么不分,极端情况 O(2^n)
运行时长:超时
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
return wordCheck(s, 0, new HashSet<String>(wordDict));
}
private boolean wordCheck(String s, int start, Set<String> wordDict){
if(start == s.length()){return true;}
for(int end = start + 1; end <= s.length(); end++){
// 此处的重复子问题 导致很多子串重复对比 效能低下。
if(wordDict.contains(s.substring(start, end)) && wordCheck(s, end, wordDict)){
return true;
}
}
return false;
}
}
方法二:自顶向下(递归+备忘录),其实就是缓存中间结果,避免重复计算。
运行时长:5ms
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
return wordCheckWithMemo(s, 0, new HashSet<String>(wordDict), new Boolean[s.length()]);
}
private boolean wordCheckWithMemo(String s, int start, Set<String> wordDict, Boolean[] memo){
if(start == s.length()){
return true;
}
if(memo[start] != null){
return memo[start];
}
for(int end = start + 1; end <= s.length(); end++){
if(wordDict.contains(s.substring(start, end)) && wordCheckWithMemo(s, end, wordDict, memo)){
return memo[start] = true;
}
}
return memo[start] = false;
}
}
方法三:自底向上(迭代推演)
运行时长:6ms
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet = new HashSet<>(wordDict);
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for(int end = 1; end <=s.length(); end++){
for(int start = 0; start < end; start++){
if(dp[start] && wordDictSet.contains(s.substring(start, end))){
dp[end] = true;
break;
}
}
}
return dp[s.length()];
}
}
方法四:广度优先搜索(Using Breadth-First-Search)
运行时长:7ms
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet = new HashSet<String>(wordDict);
boolean[] visited = new boolean[s.length()];
Queue<Integer> queue = new LinkedList<Integer>();
queue.add(0);
while(!queue.isEmpty()){
int start = queue.remove();
if(visited[start]){
continue;
}
for(int end = start+1; end <= s.length(); end++){
if(wordDictSet.contains(s.substring(start, end))){
// 凡是能往下继续走的,都加到队列中。
queue.add(end);
// 走到终点则直接返回,其他逻辑分支流程无需再走,找到一条大路通罗马即可。
if(end == s.length()){
return true;
}
}
}
// 之前尝试过的逻辑无需再走。
visited[start] = true;
}
return false;
}
}
方法五:滑动窗口的感觉!广度优先搜索 中的步长迭代策略是小步迭代,而实际上我们可以每次走 word_len,因为这样避免了无意义的“跨步行为”,让我们每一次的迭代都是货真价实的前行!
运行时长:1ms
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<Integer> visitedCache = new HashSet<Integer>();
return wordBreak(s, wordDict, 0, visitedCache);
}
private boolean wordBreak(String s, List<String> wordDict, int start, Set<Integer> visitedCache){
if(start == s.length()){
// 达到终点就返回成功!一条大路通罗马即可!
return true;
}
if(visitedCache.contains(start)){
// 走过的错路就无需再走!
return false;
}
for(String eachWord : wordDict){
// public boolean startsWith(String prefix , int toffset),其中,prefix为需要匹配的子串,toffset为字符串中开始查找的位置。
if(s.startsWith(eachWord, start) && wordBreak(s, wordDict, start + eachWord.length(), visitedCache)){
return true;
}else{
visitedCache.add(start);
}
}
return false;
}
}
方法六:自底向下的动态规划 + 预处理步长,让每一步都有意义!
运行时长:3ms
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int[] mark = new int[s.length()+1];
mark[0]=1;
List<Integer>wordsLenDict=new ArrayList<Integer>();
for(String per_word:wordDict){
if(wordsLenDict.contains(per_word.length())==false){
wordsLenDict.add(per_word.length());
}
}
for (int l = 1; l <=s.length(); l++) {
for(int w_len:wordsLenDict){
int r=l-1+w_len;
if (mark[l-1]==1&&r<=s.length()&&wordDict.contains(s.substring(l-1,r)))
mark[l-1+w_len] = 1;
}
}
return mark[s.length()] == 1;
}
}
131.Palindrome Partitioning
字符串s分割成回文子串的所有可能的组合。such as,Input: s = “aab”
Output: [[“a”,“a”,“b”],[“aa”,“b”]]
方法一:回溯法 暴力破解 尝试所有可能
运行时长:8ms
时间复杂度:当树高度为N,比如N=3(S="aaa")时候,节点个数为8个。O(N * 2^N),判别回文的时候,是 或者 否 都有两种情况,分割过程要覆盖所有可能。
空间复杂度:用N字符串s的长度,O(N)
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
List<String> eachRes = new ArrayList<>();
dfs(s, 0, res, eachRes);
return res;
}
// 回溯法 深度遍历 模拟入栈出栈 递归所有可能
private void dfs(String s, int stackHeight, List<List<String>> res, List<String> eachRes){
if(stackHeight == s.length()){
// 分割的所有子字符串 拼接成 s,则添加记录结果
res.add(new ArrayList<>(eachRes));
}
for(int newStackHeight = stackHeight; newStackHeight < s.length(); newStackHeight++){
if(isPalindrome(s, stackHeight, newStackHeight)){
// 不同长度的子字符串 入栈操作
eachRes.add(s.substring(stackHeight, newStackHeight+1));
// 深度递归 尝试放不同的新的子字符串
dfs(s, newStackHeight+1, res, eachRes);
// 栈顶的子字符串 出栈操作
eachRes.remove(eachRes.size() - 1);
}
}
}
// 判别回文
private boolean isPalindrome(String s, int start, int end){
while(start < end){
if(s.charAt(start++) != s.charAt(end--)){
return false;
}
}
return true;
}
}
错误方法二:动态规划(递归+备忘录)时间效能并没有提升!why?因为 dfs(s, newStackHeight+1, res, eachRes, memo); 深度递归的过程是由主到子,所以备忘录记录到缓存始终是“延迟的”、使用过的。
运行时长:最快8ms
时间复杂度:当树高度为N,比如N=3(S="aaa")时候,节点个数为8个。O(N * 2^N),判别回文的时候,是 或者 否 都有两种情况,分割过程要覆盖所有可能。
空间复杂度:用N字符串s的长度,O(N)
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
List<String> eachRes = new ArrayList<>();
Boolean[][] memo = new Boolean[s.length()][s.length()];
dfs(s, 0, res, eachRes, memo);
return res;
}
// 回溯法 深度遍历 模拟入栈出栈 递归所有可能
private void dfs(String s, int stackHeight, List<List<String>> res, List<String> eachRes, Boolean[][] memo){
if(stackHeight == s.length()){
// 分割的所有子字符串 拼接成 s,则添加记录结果
res.add(new ArrayList<>(eachRes));
}
for(int newStackHeight = stackHeight; newStackHeight < s.length(); newStackHeight++){
if(isPalindromeWithMemo(s, stackHeight, newStackHeight, memo)){
// 不同长度的子字符串 入栈操作
eachRes.add(s.substring(stackHeight, newStackHeight+1));
// 深度递归 尝试放不同的新的子字符串
dfs(s, newStackHeight+1, res, eachRes, memo);
// 栈顶的子字符串 出栈操作
eachRes.remove(eachRes.size() - 1);
}
}
}
// 判别回文
private boolean isPalindromeWithMemo(String s, int start, int end, Boolean[][] memo){
if(memo[start][end] != null){
return memo[start][end];
}
while(start < end){
if(s.charAt(start) != s.charAt(end)){
return memo[start][end] = false;
}
start++;
end--;
}
return memo[start][end] = true;
}
}
方法三:动态规划
运行时长:最快8ms
时间复杂度:当树高度为N,比如N=3(S="aaa")时候,节点个数为8个。O(N * 2^N),判别回文的时候,是 或者 否 都有两种情况,分割过程要覆盖所有可能。
空间复杂度:用N字符串s的长度,O(N^2)
备注:该题目中,在测试用例来看,时间性能并没有变好,反而空间复杂度变高。
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
List<String> stack = new ArrayList();
boolean[][] dp = new boolean[s.length()][s.length()];
dfs(s, res, stack, 0, dp);
return res;
}
private void dfs(String s, List<List<String>> res, List<String> stack, int stackHeight, boolean[][] dp){
if(stackHeight == s.length()){
res.add(new ArrayList(stack));
return;
}
for(int nextStackHeight = stackHeight; nextStackHeight < s.length(); nextStackHeight++){
if((s.charAt(stackHeight) == s.charAt(nextStackHeight)) && ((nextStackHeight < stackHeight + 3) || dp[stackHeight+1][nextStackHeight-1])){
dp[stackHeight][nextStackHeight] = true;
stack.add(s.substring(stackHeight, nextStackHeight + 1));
dfs(s, res, stack, nextStackHeight + 1, dp);
stack.remove(stack.size() - 1);
}
}
}
}
132. Palindrome Partitioning II
找到切刀次数最少的分割方式。比如:下面的demo切一刀就可以!
Input: s = “aab”
Output: 1 (一刀切!)
Explanation: The palindrome partitioning [“aa”,“b”] could be produced using 1 cut.
方法一:遍历所有记录下最小的切割方式 Time Limit Exceeded
class Solution {
public int minCut(String s) {
if(s.length() == 1){
return 0;
}
List<String> stack = new ArrayList<String>(s.length());
boolean[][] dp = new boolean[s.length()][s.length()];
int[] minCutTimes = new int[1];
minCutTimes[0] = Integer.MAX_VALUE;
dfs(s, stack, 0, dp, minCutTimes);
return minCutTimes[0];
}
private void dfs(String s, List<String> stack, int stackHeight, boolean[][] dp, int[] minCutTimes){
if(stackHeight == s.length()){
int curCutTimes = stack.size() - 1;
if(minCutTimes[0] > curCutTimes){
minCutTimes[0] = curCutTimes;
}
return;
}
for(int nextStackHeight = stackHeight; nextStackHeight<s.length(); nextStackHeight++){
if(s.charAt(stackHeight) == s.charAt(nextStackHeight) && ((stackHeight + 3 > nextStackHeight) || dp[stackHeight + 1][nextStackHeight - 1])){
dp[stackHeight][nextStackHeight] = true;
stack.add(s.substring(stackHeight, nextStackHeight + 1));
dfs(s, stack, nextStackHeight + 1, dp, minCutTimes);
stack.remove(stack.size() - 1);
}
}
}
}
方法二:动态规划 备忘录
class Solution {
// 最少刀数把字符串切割成多个回文子串
public int minCut(String s) {
// 备忘录
int[] mem = new int[s.length()];
// 设定备忘录初始值为-1
for(int i = 0; i<s.length(); i++){
mem[i] = -1;
}
return solve(s, 0, mem);
}
private int solve(String s, int start, int[] mem){
// 边界判断
if(s.length() == start){
return 0;
}
// 备忘录缓存
if(mem[start] != -1){
return mem[start];
}
// 初始设定切 len - 1 刀(最多刀数可能),其中 len 表示字符串长度。
int ans = s.length() - 1;
for(int end=start;end<s.length();end++){
int startIndex = start, endIndex = end;
while(startIndex <= endIndex && s.charAt(startIndex) == s.charAt(endIndex)){
startIndex++;
endIndex--;
}
if(startIndex > endIndex){
// 1. 如果抵达边界,当前这一刀就不用切了,因为后续无字符子串。
if(end == s.length() - 1){
// 问题分割为 s的start到end的最少刀数 与 s的end+1到s.length()-1的最少刀数。
// 递归逻辑是真实思考步骤的逆序,也就是最先判断和缓存的实际是s的最后两个字符的比较。
//
// ans = Math.min(ans, solve(s, end + 1, mem));
// ans = Math.min(ans, 0);
ans = 0;
}else{
// 2. 否则这一刀切下去。
ans = Math.min(ans, 1 + solve(s, end + 1, mem));
}
mem[start] = ans;
}
}
return mem[start];
}
}
55. Jump Game
从index 0 跳到 最后的index:
2表示当前步骤最多可以跳2步。
Input: nums = [2,3,1,1,4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.
Input: nums = [3,2,1,0,4]
Output: false
Explanation: You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.
class Solution {
public boolean canJump(int[] nums) {
int[] dp = new int[nums.length];
for(int i=0;i<nums.length;i++){
dp[i]=-1;
}
return canJump(nums, 0, dp) == 1;
}
private int canJump(int[] nums, int start, int[] dp){
if(start == nums.length - 1){
return dp[start] = 1;
}
if(start > nums.length - 1){
return 1;
}
if(dp[start] != -1){
return dp[start];
}
if(nums[start] == 0){
return dp[start] = 0;
}
for(int i=1; i<=nums[start]; i++){
int nextStart = start + i;
if(canJump(nums, nextStart, dp) == 1){
return dp[start] = 1;
}
}
return dp[start] = 0;
}
}
435. Non-overlapping Intervals
让子区域块之间没有重叠,移除最小的块的个数。
Input: intervals = [[1,2],[2,3],[3,4],[1,3]]
Output: 1
Explanation: [1,3] can be removed and the rest of the intervals are non-overlapping.
Input: intervals = [[1,2],[2,3]]
Output: 0
Explanation: You don't need to remove any of the intervals since they're already non-overlapping.
解法一:超时
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
int[] erase = new int[intervals.length];
return minEraseNum(intervals, 0, erase);
}
private int minEraseNum(int[][] intervals, int start, int[] erase){
if(noOverlap(intervals, erase)){
return getCountSum(erase, intervals);
}
if(start == intervals.length){
return intervals.length;
}
int res = intervals.length;
for(int i = start; i < erase.length; i++){
int noEraseIMin = minEraseNum(intervals, i + 1, erase);
erase[i] = 1;
int eraseIMin = minEraseNum(intervals, i + 1, erase);
erase[i] = 0;
int eachRes = Math.min(noEraseIMin, eraseIMin);
res = Math.min(eachRes, res);
}
return res;
}
private boolean noOverlap(int[][] intervals, int[] erase){
for(int i=0;i<intervals.length;i++){
if(erase[i] == 1){
continue;
}
for(int j=i+1;j<intervals.length;j++){
if(erase[j] == 1){
continue;
}
if(isOverlap(intervals[i], intervals[j])){
return false;
}
}
}
return true;
}
private int getCountSum(int[] erase, int[][] intervals){
int res = 0;
for(int i=0;i<intervals.length;i++){
if(erase[i] == 1){
res++;
}
}
return res;
}
private boolean isOverlap(int[] a, int[] b){
return !(a[1] <= b[0] || b[1] <= a[0]);
}
}
解法二:重复情况无法解决,错误!
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
int[][] dp = new int[intervals.length][intervals.length];
int[] count = new int[intervals.length];
int sum = 0;
for(int i = 0; i < intervals.length; i++){
for(int j = i + 1; j < intervals.length; j++){
if(isOverlap(intervals[i], intervals[j])){
dp[i][j] = 1;
dp[j][i] = 1;
sum += 2;
}
}
}
for(int i=0;i<intervals.length;i++){
for(int j=0;j<intervals.length;j++){
count[i]+=dp[i][j];
}
}
// print(dp);
// print(count);
int res = 0;
do{
int index = getMaxCountIndex(count);
if(count[index] == 0){
return res;
}
res++;
// System.out.println(index);
// System.out.println();
// print(dp);
sum -= count[index] * 2;
eraseCount(count, dp, index);
}while(sum > 0);
return res;
}
private int getMaxCountIndex(int[] count){
int value = count[0];
int index = 0;
for(int i=1;i<count.length;i++){
if(count[i] > value){
value = count[i];
index = i;
}
}
return index;
}
private void eraseCount(int[] count, int[][] dp, int index){
for(int i=0; i<count.length; i++){
if(dp[i][index] == 1){
dp[i][index] = 0;
dp[index][i] = 0;
count[i] -= 1;
}
}
}
private boolean isOverlap(int[] a, int[] b){
return !(a[1] <= b[0] || b[1] <= a[0]);
}
void print(int[][] a){
for(int i=0;i<a.length;i++){
for(int j=0;j<a[0].length;j++){
System.out.print(a[i][j]);
}
System.out.println();
}
}
void print(int[] count){
for(int i=0;i<count.length;i++){
System.out.print(count[i]);
}
}
}
解法三:
题目分析:题目中给出的[[1,2],[2,3],[3,4],[1,3]],就像一个一个的木板,长度不等,起始位置不等,现在要求去掉最少的木板个数,让木板之间没有重叠的部分。没有重叠的部分,并不一定要求木板之间没有间隙,是可以分散开来的。
1. 可以按照木板的尾巴的位置标记进行排序,然后比较第一个木板的尾巴与下一个木板的头部有没有重叠,如果有重叠,就剔除掉,换下一个。
2. 如果没有重叠,则可以将前一个木板的尾巴顺延至下一个木板,然后继续看与后续木板有没有重叠。
策略:右对齐,一块一块往前面拼接,留下短的,移走长的。
class Solution {
public int eraseOverlapIntervals(int[][] plank) {
if(plank.length == 1){
// 一个木板直接返回,无需移除。
return 0;
}
Arrays.sort(plank, Comparator.comparingInt(x->x[1]));
int currentPlankIndex = 0;
int headIndex = 0, tailIndex = 1;
int removePlankCount = 0;
for(int i = currentPlankIndex + 1; i < plank.length; i++){
if(plank[currentPlankIndex][tailIndex] > plank[i][headIndex]){
// 如果当前木板的尾巴比下一个木板的头部大,说明木板之间存在重叠,则移除。
removePlankCount++;
}else{
currentPlankIndex = i;
}
}
return removePlankCount;
}
}
解法四:贪心算法
贪心算法思想:(根据题意选取一种量度标准,做出在当前看来是最好的选择!)
总是做出当前看来最好的选择。不从整体最优上考虑,算法得到的是在某种局部意义上的最优解。所以贪心算法不要回溯。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择。
不能保证解是最佳的。因为贪心算法总是从局部出发,并没从整体考虑。
贪心算法一般用来解决求最大或最小解。
贪心算法只能确定某些问题的可行性范围。
回溯是深度的递归遍历所有可能。
动态规划是空间换时间,迭代计算缓存思想优化重叠子问题。
举例:
例如,平时购物找零钱时,为使找回的零钱的硬币数最少,不要求找零钱的所有方案,而是从最大面值的币种开始,按递减的顺序考虑各面额,先尽量用大面值的面额,当不足大面值时才去考虑下一个较小面值,这就是贪心算法。
其中,先尽量用大面值的面额 就是一种局部最优的策略。
策略:左对齐,一块一块往后面拼接,留下短的,移走长的。
class Plank{
int head;
int tail;
public Plank(int head, int tail){
this.head = head;
this.tail = tail;
}
public boolean isOverLap(Plank plank){
return !(this.tail <= plank.head || this.head >= plank.tail);
}
}
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
PriorityQueue<Plank> priorityQueue = new PriorityQueue<>(new Comparator<>(){
public int compare(Plank plankA, Plank plankB){
return (plankA.head != plankB.head ? plankA.head - plankB.head : plankA.tail - plankB.tail);
}
});
for(int i = 0; i < intervals.length; i++){
priorityQueue.add(new Plank(intervals[i][0], intervals[i][1]));
}
Stack<Plank> stack = new Stack<Plank>();
while(!priorityQueue.isEmpty()){
Plank curPlank = priorityQueue.poll();
if(stack.isEmpty() || !stack.peek().isOverLap(curPlank)){
stack.add(curPlank);
}else if(curPlank.tail <= stack.peek().tail){
stack.pop();
stack.add(curPlank);
}
}
return intervals.length - stack.size();
}
}
241. Different Ways to Add Parentheses
题目:
任意选择计算子集的顺序。
Input: expression = "2-1-1"
Output: [0,2]
Explanation:
((2-1)-1) = 0
(2-(1-1)) = 2
Input: expression = "2*3-4*5"
Output: [-34,-14,-10,-10,10]
Explanation:
(2*(3-(4*5))) = -34
((2*3)-(4*5)) = -14
((2*(3-4))*5) = -10
(2*((3-4)*5)) = -10
(((2*3)-4)*5) = 10
方案一:15ms
-
思路:2-1-1-1-1,把符号和数字提取出来。把所有的运算看成是符号下标的全排列组合。注意,由于是通过括号的方式实现运算次序的变化,所以此处需要考虑
个别排列组合无法达成的情况
。 -
正序情况,通过括号可以正常实现。
-
逆序情况,上述符号下标为:1,3,5,7。当排列组合1 7 3 5时,无法通过括号实现。即新加入的符号的下标,如果小于栈顶的下标(比如:7之后加3),则要判别3到7过程中的元素是否都在栈里面(是否可以通过括号方式触达),3 5 7,此时5没有入栈(括号不连续),所以此种情况要从全排列中剔除掉。同理,5 7 3 1是可以实现的,此种排列对应的表达式为:2-(1-((1-1)-1))。
class Solution { public List<Integer> diffWaysToCompute(String expression) { List<Integer> symbolIndexsList = new ArrayList<>(); List<Integer> numsList = new ArrayList<>(); splitSymbolIndexAndNum(expression, symbolIndexsList, numsList); // list 转 array int[] symbolIndexs = Arrays.stream((Integer[])symbolIndexsList.toArray(new Integer[symbolIndexsList.size()])).mapToInt(Integer::valueOf).toArray(); int[] nums = Arrays.stream((Integer[])numsList.toArray(new Integer[numsList.size()])).mapToInt(Integer::valueOf).toArray(); List<Integer> result = new ArrayList<>(); if(nums.length == 1){ result.add(nums[0]); return result; } List<Integer> stack = new ArrayList<>(); List<List<Integer>> allCombine = new ArrayList<>(); boolean[] symbolUsed = new boolean[symbolIndexs.length]; getAllCombineNew(symbolIndexs, stack, allCombine, symbolUsed); // 并查集 记录哪些值需要被覆盖更新 for(List<Integer> eachCalWay : allCombine){ int eachRes = getEachRes(expression, eachCalWay, getCopyNums(nums), getUnion(nums.length), symbolIndexs); result.add(eachRes); } return result; } private int[] getCopyNums(int[] nums){ int[] copyNums = new int[nums.length]; for(int i=0;i<nums.length;i++){ copyNums[i] = nums[i]; } return copyNums; } private int getEachRes(String expression, List<Integer> eachCalWay, int[] nums, int[] union, int[] symbolIndexs){ int res = 0; for(int i=0;i<eachCalWay.size();i++){ int symbolPos = eachCalWay.get(i); char symbol = expression.charAt(symbolPos); int calIndexPos = getCalIndexPos(symbolPos, symbolIndexs); res = calAndSet(symbol, nums, calIndexPos, union); } return res; } // 获取符号在 expression 中的位置。 private int getCalIndexPos(int symbolPos, int[] symbolIndexs){ for(int i=0;i<symbolIndexs.length;i++){ if(symbolPos == symbolIndexs[i]){ return i*2+1; } } return 0; } // 四则运算。 private int calAndSet(char symbol, int[] num, int symbolPos, int[] union){ int res = 0; switch(symbol){ case '+':res = num[symbolPos-1] + num[symbolPos+1];break; case '-':res = num[symbolPos-1] - num[symbolPos+1];break; case '*':res = num[symbolPos-1] * num[symbolPos+1];break; case '/':res = num[symbolPos-1] / num[symbolPos+1];break; } letThemSameParent(union, symbolPos-1, symbolPos+1); setAllSameParent(num, symbolPos-1, union, res); return res; } // 得到所有的组合 // private void getAllCombine(int[] symbol, List<Integer> stack, List<List<Integer>> allCombine, boolean[] used){ // if(stack.size() == symbol.length){ // allCombine.add(new ArrayList<>(stack)); // // System.out.println(stack.toString()); // return; // } // for(int i=0;i<symbol.length;i++){ // if(!Boolean.TRUE.equals(used[i])){ // stack.add(symbol[i]); // used[i] = true; // getAllCombine(symbol, stack, allCombine, used); // stack.remove(stack.size()-1); // used[i] = false; // } // } // } // 得到所有的组合 排除 312 的情况,因为括号无法实现 312 的次序。4213 也无法实现,所以剔除掉 倒序非相邻的情况。 private void getAllCombineNew(int[] symbolIndexs, List<Integer> stack, List<List<Integer>> allCombine, boolean[] used){ if(stack.size() == symbolIndexs.length){ allCombine.add(new ArrayList<>(stack)); return; } for(int i=0;i<symbolIndexs.length;i++){ if(!Boolean.TRUE.equals(used[i])){ if(check(stack, symbolIndexs[i], symbolIndexs)){ stack.add(symbolIndexs[i]); used[i] = true; getAllCombineNew(symbolIndexs, stack, allCombine, used); stack.remove(stack.size()-1); used[i] = false; } } } } // 实际上是遍历奇数的所有可能的全排列组合。 // 判断加入栈中的新的 wantAddedIndex: // 1. 比 栈顶元素 popStackIndex大,则返回 true。 // 2. 比 栈顶元素 popStackIndex小,则是倒序。 // 2.1 如果新加入的元素 到 栈顶的元素,之间的元素都在栈里面,返回true。 // 2.2 如果不相邻返回false。 private boolean check(List<Integer> stack, int wantAddedIndex, int[] symbolIndexs){ if(stack.size() == 0 || stack.size() == symbolIndexs.length - 1){ return true; } int popStackIndex = stack.get(stack.size() - 1); int bottomStackIndex = stack.get(0); // 正序 if(wantAddedIndex > popStackIndex){ return true; } int popStackIndexPos = 0; for(int i=0;i<symbolIndexs.length;i++){ if(popStackIndex == symbolIndexs[i]){ popStackIndexPos = i; break; } } int wantAddedIndexPos = 0; for(int i=0;i<symbolIndexs.length;i++){ if(wantAddedIndex == symbolIndexs[i]){ wantAddedIndexPos = i; break; } } // 逆序:想要加入的元素,比当前栈顶元素小。 // 连续性质的考核:新加入的元素 到 栈顶的元素,之间的元素都在栈里面。说明该逆序情况是可以触达的。 if(wantAddedIndexPos + 1 < symbolIndexs.length){ while(symbolIndexs[++wantAddedIndexPos] != popStackIndex){ if(!stack.contains(symbolIndexs[wantAddedIndexPos])){ return false; } } return true; } return false; } // 字符数组,偶数是数字,奇数是符号。字符和数字进行分离获取。 private void splitSymbolIndexAndNum(String expression, List<Integer> symbolIndexsList, List<Integer> numsList){ int symbolIndex = 0, numsIndex = 0, lastSymbolIndex = -1; for(int i=0;i<expression.length();i++){ if(isSymbol(expression.charAt(i))){ symbolIndexsList.add(i); numsList.add(Integer.parseInt(expression.substring(lastSymbolIndex + 1, i))); // -1 占位符号的地方 numsList.add(-1); lastSymbolIndex = i; } } if(lastSymbolIndex+1 < expression.length()){ numsList.add(Integer.parseInt(expression.substring(lastSymbolIndex + 1, expression.length()))); } } // 符号字符判别。 private boolean isSymbol(char s){ switch(s){ case '+': case '-': case '*': case '/':return true; } return false; } // 并查集:初始化。 private int[] getUnion(int len){ int[] union = new int[len]; for(int parentIndex=0;parentIndex<len;parentIndex++){ union[parentIndex] = parentIndex; } return union; } // parentIndex 的原因是 父节点只有一个。childrenIndex 是一对多关系,无法用数组刻画。 private int getUnionParentIndex(int[] union, int curIndex){ int parentIndex = curIndex; // 查找父节点 while(union[parentIndex] != parentIndex){ parentIndex = union[parentIndex]; } // 优化路径,压缩指向同一个源头。 while(union[curIndex] != curIndex){ curIndex = union[curIndex]; union[curIndex] = parentIndex; } return parentIndex; } // 并查集:同源判别。 private boolean sameParent(int[] union, int indexA, int indexB){ if(indexA == indexB){ return true; } int parentIndexA = getUnionParentIndex(union, indexA); int parentIndexB = getUnionParentIndex(union, indexB); return parentIndexA == parentIndexB; } // 借助并查集,更新覆盖同源变更。 private void setAllSameParent(int[] num, int index, int[] union, int newValue){ for(int i=0; i<num.length; i++){ if(sameParent(union, index, i)){ num[i] = newValue; } } } // 并差集:同源指定。 private void letThemSameParent(int[] union, int indexA, int indexB){ int parentIndex = getUnionParentIndex(union, indexA); union[indexB] = parentIndex; } }
方案二:10ms
-
递归法,由于最优子结构的特性,可以考虑进行问题划分:
class Solution { public List<Integer> diffWaysToCompute(String expression) { return partDiffWaysToCompute(expression, 0, expression.length()); } private List<Integer> partDiffWaysToCompute(String expression, int start, int end){ // if(start == end){ // return Arrays.asList(expression.charAt(start) - '0'); // } List<Integer> res = new ArrayList<>(); for(int i = start; i < end; i++){ char curChar = expression.charAt(i); // 如果当前字符是 符号,则可以进行两侧的分割。-》()符号() if(curChar < '0' || curChar > '9'){ List<Integer> leftPartRes = partDiffWaysToCompute(expression, start, i); List<Integer> rightPartRes = partDiffWaysToCompute(expression, i+1, end); // 穷举求和 for(Integer eachLeftPartRes : leftPartRes){ for(Integer eachRightPartRes : rightPartRes){ switch(curChar){ case '+': res.add(eachLeftPartRes + eachRightPartRes);break; case '-': res.add(eachLeftPartRes - eachRightPartRes);break; case '*': res.add(eachLeftPartRes * eachRightPartRes);break; case '/': res.add(eachLeftPartRes / eachRightPartRes);break; } } } } } return res.size() > 0 ? res : Arrays.asList(Integer.valueOf(expression.substring(start, end))); } }
方案三:7ms
-
加个缓存备忘录
class Solution { public List<Integer> diffWaysToCompute(String expression) { Map<String, List<Integer>> memo = new HashMap<>(); return partDiffWaysToCompute(expression, 0, expression.length(), memo); } private List<Integer> partDiffWaysToCompute(String expression, int start, int end, Map<String, List<Integer>> memo){ // if(start == end){ // return Arrays.asList(expression.charAt(start) - '0'); // } String key = getKey(start, end); if(memo.containsKey(key)){ return memo.get(key); } List<Integer> res = new ArrayList<>(); for(int i = start; i < end; i++){ char curChar = expression.charAt(i); // 如果当前字符是 符号,则可以进行两侧的分割。-》()符号() if(curChar < '0' || curChar > '9'){ List<Integer> leftPartRes = partDiffWaysToCompute(expression, start, i, memo); List<Integer> rightPartRes = partDiffWaysToCompute(expression, i+1, end, memo); // 穷举求和 for(Integer eachLeftPartRes : leftPartRes){ for(Integer eachRightPartRes : rightPartRes){ switch(curChar){ case '+': res.add(eachLeftPartRes + eachRightPartRes);break; case '-': res.add(eachLeftPartRes - eachRightPartRes);break; case '*': res.add(eachLeftPartRes * eachRightPartRes);break; case '/': res.add(eachLeftPartRes / eachRightPartRes);break; } } } } } res = res.size() > 0 ? res : Arrays.asList(Integer.valueOf(expression.substring(start, end))); memo.put(key, res); return res; } private String getKey(int start, int end){ return "" + start + '_' + end; } }
方案四:1ms
-
备忘录的确有缓存优化效果,但是耗费时间的实际是字符串的charAt操作,所以直接substring进行截断操作,减法思想,让运算愈发精简。
class Solution { public List<Integer> diffWaysToCompute(String expression) { Set<Character> symbolSet = new HashSet<>(Arrays.asList('*', '+', '-')); Map<String, List<Integer>> memo = new HashMap<>(); return partDiffWaysToCompute(symbolSet, expression, memo); } private List<Integer> partDiffWaysToCompute(Set<Character> symbolSet, String expression, Map<String, List<Integer>> memo){ // if(start == end){ // return Arrays.asList(expression.charAt(start) - '0'); // } // String key = getKey(start, end); if(memo.containsKey(expression)){ return memo.get(expression); } List<Integer> res = new ArrayList<>(); for(int i = 0; i < expression.length(); i++){ char curChar = expression.charAt(i); // 如果当前字符是 符号,则可以进行两侧的分割。-》()符号() // if(curChar < '0' || curChar > '9'){ if(symbolSet.contains(curChar)){ List<Integer> leftPartRes = partDiffWaysToCompute(symbolSet, expression.substring(0, i), memo); List<Integer> rightPartRes = partDiffWaysToCompute(symbolSet, expression.substring(i + 1), memo); // 穷举求和 for(Integer eachLeftPartRes : leftPartRes){ for(Integer eachRightPartRes : rightPartRes){ switch(curChar){ case '+': res.add(eachLeftPartRes + eachRightPartRes);break; case '-': res.add(eachLeftPartRes - eachRightPartRes);break; case '*': res.add(eachLeftPartRes * eachRightPartRes);break; case '/': res.add(eachLeftPartRes / eachRightPartRes);break; } } } } } res = res.size() > 0 ? res : Arrays.asList(Integer.valueOf(expression)); memo.put(expression, res); return res; } // private String getKey(int start, int end){ // return "" + start + '_' + end; // } }
464. Can I Win:
-
两个人,轮流取数(比如:1到10,不可重复使用,可以任意随心取,两者取得的数放到一个篮子里),如果第一个人拿到数后先凑够了(大于等于)预期(比如:11),则第一个人获胜,否则第二个人获胜。问是否第一个人能够先触达预期取得胜利。(是一个博弈的过程,每个人都希望自己可以取胜)
Input: maxChoosableInteger = 10, desiredTotal = 11 Output: false Explanation: No matter which integer the first player choose, the first player will lose. The first player can choose an integer from 1 up to 10. If the first player choose 1, the second player can only choose integers from 2 up to 10. The second player will win by choosing 10 and get a total = 11, which is >= desiredTotal. Same with other integers chosen by the first player, the second player will always win.
方案一:TimeOut
-
思路:递归回溯。
class Solution { public boolean canIWin(int maxChoosableInteger, int desiredTotal) { int sum = (1 + maxChoosableInteger) * maxChoosableInteger << 1; if(sum < desiredTotal){ return false; } Set<Integer> allChoosedNum = new HashSet<>(); return canWin(maxChoosableInteger, desiredTotal, allChoosedNum); } private boolean canWin(int maxChoosableInteger, int desiredTotal, Set<Integer> allChoosedNum){ for(int i = maxChoosableInteger; i > 0; i--){ if(!allChoosedNum.contains(i)){ if(i >= desiredTotal){ // System.out.println("allChoosedNum:"+allChoosedNum.toString()); return true; } allChoosedNum.add(i); // 对手没有赢的过程中,会去试错。对手成功返回,当前选手需要继续重试,所以该方法内也需要回溯。 if(!canWin(maxChoosableInteger, desiredTotal - i, allChoosedNum)){ // 此处也需要进行回溯,因为递归是深度遍历所有的情况,直到找到一种可能情况,如果不回溯会导致其他可能路径的错乱,最终影响整体的结果。 allChoosedNum.remove(i); return true; } allChoosedNum.remove(i); } } return false; } }
方案二:968 ms
-
思路:递归回溯+备忘录+位移操作+掩码信息标识(记录当前数字是否被使用过)
-
注意:先缓存当前结果,再递归!注意思考DFS的路径推演变化。
class Solution { public boolean canIWin(int maxChoosableInteger, int desiredTotal) { // 位移操作。 int sum = (1 + maxChoosableInteger) * maxChoosableInteger >> 1; // 累加求和 也 无法触达 desiredTotal if(sum < desiredTotal){ return false; } // 备忘录 Map<Integer, Boolean> memo = new HashMap<>(); // Integer作为 掩码信息标识(记录当前数字是否被使用过) return canWin(maxChoosableInteger, desiredTotal, 0, memo); } private boolean canWin(int maxChoosableInteger, Integer desiredTotal, Integer maskInfoSign, Map<Integer, Boolean> memo){ for(int i = maxChoosableInteger; i > 0; i--){ int used = (1 << i) & maskInfoSign; // 使用过则越过 if(used != 0){ continue; } if(i >= desiredTotal){ // System.out.println("allChoosedNum:"+allChoosedNum.toString()); return true; } maskInfoSign |= 1 << i; if(!memo.containsKey(maskInfoSign)){ // 备忘要备忘所有可能,包括错误路径,防止无意义的重复计算,缓存优化重复子问题(对和错的路径都有)。 memo.put(maskInfoSign, !canWin(maxChoosableInteger, desiredTotal - i, maskInfoSign, memo)); // 此种写法则会超时:put依赖递归过程的值,递归过程需要做缓存,所以要先put,再递归。 // boolean res = !canWin(maxChoosableInteger, desiredTotal - i, usedBitRecord, memo); // if(!memo.containsKey(usedBitRecord)){ // memo.put(usedBitRecord, res); // } } if(memo.get(maskInfoSign)){ return true; } maskInfoSign &= ~(1 << i); } return false; } }
方案三:TimeOut
-
思路:递归+备忘录+标记数组
class Solution { public boolean canIWin(int maxChoosableInteger, int desiredTotal) { int sum = maxChoosableInteger * (maxChoosableInteger + 1) >> 1; if(sum < desiredTotal){ return false; } int[] used = new int[maxChoosableInteger + 1]; Map<String, Boolean> memo = new HashMap<>(); return canWin(maxChoosableInteger, desiredTotal, used, memo); } private boolean canWin(int maxChoosableInteger, int desiredTotal, int[] used, Map<String, Boolean> memo){ String key = Arrays.toString(used); if(memo.containsKey(key)){ return memo.get(key); } for(int i = maxChoosableInteger; i > 0; --i){ if(used[i] == 1){ continue; } used[i] = 1; if(i >= desiredTotal || !canWin(maxChoosableInteger, desiredTotal - i, used, memo)){ memo.put(key, true); used[i] = 0; return true; } used[i] = 0; } memo.put(key, false); return false; } }
553. Optimal Division
-
题目:通过括号控制运算优先级,让最终运算结果最大。即最优除法。
Input: nums = [1000,100,10,2] Output: "1000/(100/10/2)" Explanation: 1000/(100/10/2) = 1000/((100/10)/2) = 200 However, the bold parenthesis in "1000/((100/10)/2)" are redundant, since they don't influence the operation priority. So you should return "1000/(100/10/2)". Other cases: 1000/(100/10)/2 = 50 1000/(100/(10/2)) = 50 1000/100/10/2 = 0.5 1000/100/(10/2) = 2
-
解法一:
// 全排列 // public void getAllCombineDivision(int[] nums, List<List<Integer>> allCombine, List<Integer> stack, boolean[] used){ // if(stack.size() == nums.length){ // allCombine.add(new ArrayList<>(stack)); // } // for(int i=0; i<nums.length; i++){ // if(!used[i]){ // stack.add(nums[i]); // used[i] = true; // getAllCombineDivision(nums, allCombine, stack, used); // stack.remove(stack.size() - 1); // used[i] = false; // } // } // } // 每个都是两部分,最大分子 和 最小分母。 思路:a/(b/c/d/....)是最终的结果。进行拼接就好。 class Solution { public String optimalDivision(int[] nums) { if(nums.length == 1){ return "" + nums[0]; } if(nums.length == 2){ return "" + nums[0] + "/" + nums[1]; } String res = "" + nums[0] + "/("; for(int i = 1; i<nums.length - 1; i++){ res += "" + nums[i] + "/"; } res += "" + nums[nums.length - 1] + ")"; return res; } }
leetCode国际版类型题
- \139. Word Break:https://leetcode.com/problems/word-break/
- \131. Palindrome Partitioning:https://leetcode.com/problems/palindrome-partitioning/
- \132. Palindrome Partitioning II:https://leetcode.com/problems/palindrome-partitioning-ii/
- \55. Jump Game:https://leetcode.com/problems/jump-game/
- \435. Non-overlapping Intervals:https://leetcode.com/problems/non-overlapping-intervals/
- \241. Different Ways to Add Parentheses:https://leetcode.com/problems/different-ways-to-add-parentheses/
- \464. Can I Win:https://leetcode.com/problems/can-i-win/
- \553. Optimal Division:https://leetcode.com/problems/optimal-division/
Reference
- https://zhuanlan.zhihu.com/p/126124250(经典算法系列:动态规划)
- https://baike.baidu.com/item/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95/5411800(贪心算法)