因为直接去递归next会改变顺序,所以可以每两个结点下去递归
还有链表的那一套~
这题可以有不是递归的方法 借助辅助数组另外使用双指针
public void reorderList(ListNode head) {
if(head ==null || head.next ==null) return ;
List<ListNode> nodes = new ArrayList<>();
ListNode cur = head;
while(cur !=null){
nodes.add(cur);
cur = cur.next;
}
int i =0;
int j = nodes.size()-1;
while(i<j){
nodes.get(i).next = nodes.get(j);
i++;
if(i >= j){
break;
}
nodes.get(j).next = nodes.get(i);
j--;
}
nodes.get(i).next = null;
}
递归方法相当于中间段处理好了,返回了这个头结点对应的tail。穿起来。返回外层head 的tail.
递归的重点在于每次都是返回tail
public void reorderList(ListNode head) {
if(head==null ||head.next==null||head.next.next==null) return ;
int len = 0;
ListNode cur = head;
while(cur!=null){
len++;
cur = cur.next;
}
reverse(head,len);
}
public ListNode reverse(ListNode head,int len){
if(len ==1){
ListNode tail = head.next;
head.next = null;
return tail;
}
if(len==2){
ListNode tail = head.next.next;
head.next.next = null;
return tail;
}
ListNode tail = reverse(head.next,len-2);
ListNode post = tail.next;
tail.next = head.next;
head.next = tail;
return post;
}
integer 和int的取值范围一样,为什么int这里会报错呢?
public boolean isValidBST(TreeNode root) {
return isBST(root, null, null);
}
public boolean isBST (TreeNode root, Integer lower, Integer upper){
if(root == null) return true;
int val = root.val;
if(lower != null && val <= lower) return false;
if(upper!= null && val >= upper) return false;
if(! isBST(root.left, lower, val)) return false;
if(!isBST(root.right, val,upper)) return false;
return true;
}
至今都还每写对,仍然有问题;
//1- k-1 作为左子树,k+1 -n作为右子树,最后等于左子树的个数 * 右子树的个数
//缺少抽象提取信息的能力
public int numTrees(int n) {
if(n ==1 || n==0) return 1;
int res =0;
for(int i =1;i<=n;i++){
//其实无所谓递归的数具体是几,只是希望得到这几个数能组成的二叉搜索树的个数
res += numTrees(i-1) * numTrees(n-i);
}//看这里,对前面状态的依赖,重复的计算----->DP正合适
return res;
}
DP方法
//s得再想象一下
//缺少抽象提取信息的能力
public int numTrees(int n) {
if(n ==1 || n==0) return 1;
int[] dp = new int[n+1];
//状态是数字为i时有多少种状态
dp[0] = 1;
dp[1] = 1;
for(int i = 2;i<=n;i++){
for(int j =1;j<=i;j++){
dp[i] += dp[j-1] * dp[i-j];
}
}
return dp[n];
}
class Solution {
public void flatten(TreeNode root) {
List<TreeNode> list = new ArrayList<TreeNode>();
preorderTraversal(root, list);
int size = list.size();
for (int i = 1; i < size; i++) {
TreeNode prev = list.get(i - 1), curr = list.get(i);
prev.left = null;
prev.right = curr;
}
}
public void preorderTraversal(TreeNode root, List<TreeNode> list) {
if (root != null) {
list.add(root);
preorderTraversal(root.left, list);
preorderTraversal(root.right, list);
}
}
//非递归的先序遍历
}
我想要的递归:
//本来只要dfs遍历就可以,但这里要求原地
//理解什么是原地,就是不能去新建一棵树,要在这棵树的基础上改
// 这才是我想要的代码,完美递归,其实仔细想想应该能想到的,,可惜了
class Solution {
public void flatten(TreeNode root) {
if(root == null) return ;
flatten(root.left);
TreeNode r = root.right;
root.right = root.left;
root.left = null;
while(root.right!=null){
root = root.right;
}
flatten(r);
root.right = r;
}
}
组合、排列、子集、路径(求各种可能的情况的这种题)都可以用回溯。
回溯 其实就是DFS搜索的过程,只是DFS 特指这种用在树上的搜索。
另一个角度理解,回溯其实就是在生成一个搜索树,所以其实回溯和dfs思想一样,可以理解为dfs是一种特殊的回溯。。
link.
这里的回溯讲的很清楚:
解决一个回溯问题,实际上就是一个决策树的遍历过程。一般来说,需要解决三个问题:
我们所使用的框架基本就是:
LinkedList result = new LinkedList();
public void backtrack(路径,选择列表){
if(满足结束条件){
result.add(结果);
}
for(选择:选择列表){
做出选择;
backtrack(路径,选择列表);
撤销选择;
}
}
其中最关键的点就是:在递归之前做选择,在递归之后撤销选择。
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
List<String> res = new LinkedList<>();
public List<String> generateParenthesis(int n) {
dfs(new StringBuilder(),0,0,n);
return res;
}
public void dfs(StringBuilder s, int left, int right,int n){
if(right>left || left>n) return ;
if(s.length()== 2*n){
res.add(s.toString());
return ;
}
s.append("(");
//注意这里的坑,不能用++,那是赋值
dfs(s,left+1,right,n);
s.deleteCharAt(s.length()-1);
s.append(")");
dfs(s,left,right+1,n);
s.deleteCharAt(s.length()-1);
}
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
//不能重复,记得剪
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
dfs(candidates, 0,target,0, new LinkedList<Integer>());
return res;
}
//注意剪枝的方式,就是每次都只考虑当前点以及之后的点,因为前面的在当前点考虑就重复了
public void dfs(int[] nums,int index, int target, int sum, LinkedList<Integer> cur){
if(sum > target) return ;
if(sum == target){
//注意这里不能直接添加,因为引用类型,后面还会改变
res.add(new ArrayList(cur));
return;
}
for(int i = index; i< nums.length;i++){
cur.add(nums[i]);
dfs(nums, i,target,sum+nums[i],cur);
cur.removeLast();
}
}
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
//与39的区别是,1 有重复数字 2 每个数字只能使用一次
//两种list都有Collection参数的构造函数
Arrays.sort(candidates);
dfs(candidates,0,0,target,new LinkedList<Integer>());
return res;
}
public void dfs(int[] nums, int index,int sum,int target, LinkedList<Integer> cur){
if(sum > target) return;
if(sum == target){
res.add(new LinkedList(cur));
return ;
}
for(int i = index;i< nums.length;i++){
if(sum > target) return ;
if(sum == target){
res.add(new LinkedList(cur));
return ;
}
//i>0 这样剪枝把下一层相同的数剪了,这是不对的,重复的是同一层的相等的数
if(i>index && nums[i]==nums[i-1]) continue;
cur.add(nums[i]);
dfs(nums,i+1, sum+nums[i], target, cur);
cur.removeLast();
}
}
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
这题其实相当于一个九叉树的遍历了,只是为了不取重复数字而遍历当前位置的后面的数字。
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
//注意这里写起来有点别扭
dfs(n,k,1,0,new LinkedList<Integer>());
return res;
}
public void dfs(int n, int k, int curNum, int curSum, LinkedList<Integer> tmp){
if(curSum > n) return ;
if(curSum== n && tmp.size()==k ){
res.add(new LinkedList(tmp));
return ;
}
//为了去重,可以规定每个数只能搭配它后面的
for(int i = curNum; i<= 9;i++){
tmp.add(i);
dfs(n,k,i+1,curSum+i,tmp);
tmp.removeLast();
}
}
}
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
dfs(new LinkedList<Integer>(),nums);
return res;
}
public void dfs(LinkedList<Integer> cur, int[] nums){
if(cur.size()== nums.length){
res.add(new ArrayList(cur));
}
for( int i =0;i< nums.length;i++){
if(cur.contains(nums[i])) continue;
cur.add(nums[i]);
dfs(cur,nums);
cur.removeLast();
}
}
要注意剪枝方法和辅助数组法
nums[i]==nums[i-1] && ! used[i-1] 说明这种情况已经被考虑过了。可以理解为重复的元素在同一层的情况,肯定是会重复的。
//这题在写的时候,就是不知道每次加数进去的时候数字用没用过,因为有重复也不能用contains判断了
//所以直接每次都全遍历,单用used数组标志用没用过
//不知道怎么处理,肯定是缺东西了,加辅助数组,临时变量都是方法
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums){
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
dfs(new LinkedList(), nums, used);
return res;
}
public void dfs(LinkedList<Integer> cur, int[] nums, boolean[] used ){
if(cur.size()== nums.length){
res.add(new ArrayList(cur));
return ;
}
for(int i=0;i<nums.length;i++){
if(used[i]){
continue;
}
if(i>0 && nums[i]==nums[i-1] && ! used[i-1]){
continue;
}
used[i] = true;
cur.add(nums[i]);
dfs(cur,nums,used);
used[i] = false;
cur.removeLast();
}
}
class Solution {
//完全可以按照47的思路来
List<String> res = new LinkedList<>();
char[] c;
public String[] permutation(String s) {
c = s.toCharArray();
Arrays.sort(c);
boolean[] used = new boolean[c.length];
dfs(c,new StringBuilder(),used);
return res.toArray(new String[res.size()]);
}
void dfs(char[] c, StringBuilder sb,boolean[] used) {
if(sb.length() == c.length){
res.add(new String(sb));
return;
}
for(int i = 0; i < c.length; i++){
if(used[i]){
continue;
}
if(i > 0 && c[i] == c[i-1] && !used[i-1]){
continue;
}
used[i] = true;
sb.append(c[i]);
dfs(c,sb,used);
used[i] = false;
sb.delete(sb.length()-1,sb.length());
}
}
}
class Solution {
//组合好想,因为从前往后选就行了
//但是排列是在某个位置选完后,后面的位置要从没选过的元素中选择
//没选过的元素位置不固定啊
//这里的交换。。。不懂
List<String> res = new LinkedList<>();
char[] c;
public String[] permutation(String s) {
c = s.toCharArray();
dfs(0);
return res.toArray(new String[res.size()]);
}
void dfs(int x) {
if(x == c.length - 1) {
res.add(String.valueOf(c)); // 添加排列方案
return;
}
HashSet<Character> set = new HashSet<>();
for(int i = x; i < c.length; i++) {
if(set.contains(c[i])) continue; // 重复,因此剪枝
set.add(c[i]);
swap(i, x); // 交换,将 c[i] 固定在第 x 位
dfs(x + 1); // 开启固定第 x + 1 位字符
swap(i, x); // 恢复交换
}
}
void swap(int a, int b) {
char tmp = c[a];
c[a] = c[b];
c[b] = tmp;
}
}
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
class Solution {
List<List<String>> res = new LinkedList<>();
public List<List<String>> partition(String s) {
//if(s==null||s.length()==1) return true;
dfs(s, 0, new LinkedList());
return res;
}
public void dfs(String s, int start, LinkedList<String> cur){
if(start == s.length()){
res.add(new LinkedList<>(cur));
return;
}
for(int i = start; i< s.length(); i++){
if(!isParalism(s,start,i)){
continue;
}
cur.addLast(s.substring(start,i+1));
dfs(s, i+1, cur);
cur.removeLast();
}
}
public boolean isParalism(String c, int start, int end){
if(end==start) return true;
while(start<end){
if(c.charAt(start)!= c.charAt(end)){
return false;
}
start++;
end--;
}
return true;
}
}
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
//这个题不会做的点是不知道什么时候可以结束递归啊
//什么时候当前结果结束,可以放在结果集中?
List<List<Integer>> res = new LinkedList<>();
int len =0;
public List<List<Integer>> subsets(int[] nums) {
// res.add(new LinkedList<>());
dfs(nums,0,0,new LinkedList<>());
return res;
}
public void dfs(int[] nums, int len, int index, LinkedList<Integer> cur){
if(cur.size() == len){
res.add(new LinkedList<>(cur));
}
for(int i = index;i<nums.length;i++){
cur.add(nums[i]);
dfs(nums, len+1,i+1,cur);
cur.removeLast();
}
}
1 暴力思路
//自己的思路:找到所有的子集分割的情况,然后判断相等的
//关键是怎么找到所有情况的dfs也写不出来,因为没读懂隐含的条件
//两个等和子集,说明每个子集的和都是 nums 数组和的一半
//超时但却思路完整的代码
public boolean canPartition(int[] nums) {
if(nums.length==1) return false;
int total = 0;
for(int num: nums) total += num;
if(total % 2 != 0) return false;
//接下来的过程就是找 total/2的子集了;
if(dfs(nums, 0, total /2)) return true;
return false;
}
public boolean dfs(int[] nums, int index, int cur){
if(cur==0) return true;
for(int i = index;i<nums.length;i++){
if(cur-nums[i]<0) return false;
if(dfs(nums,i+1, cur-nums[i])) return true;
//这其实也是回溯,但是因为参数传cur,所以值传到下一层但这层的值没有改变
}
return false;
}
2 暴力会超时,经过剪枝叶,可以ac
//子集中可以有重复数字,但是这个重复不应该在同一层中,否则后面的这个重复的就属于多考虑的了
//于是那个问题又出现了,就是怎么去掉同一层重复的,而保留下一层重复
public boolean canPartition(int[] nums) {
Arrays.sort(nums);
if(nums.length==1) return false;
int total = 0;
for(int num: nums) total += num;
if(total % 2 != 0) return false;
//接下来的过程就是找 total/2的子集了;
if(dfs(nums, 0, total /2)) return true;
return false;
}
public boolean dfs(int[] nums, int index, int cur){
if(cur==0) return true;
for(int i = index;i<nums.length;i++){
//控制一下index,就可以保证只在这一层控制重复的问题
if(i-1 >= index && nums[i]==nums[i-1]) continue;
if(cur-nums[i]<0) return false;
if(dfs(nums,i+1, cur-nums[i])) return true;
//这其实也是回溯,但是因为参数传cur,所以值传到下一层但这层的值没有改变
}
return false;
}
3 背包 动态规划
//01背包wenti
//dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j。
public boolean canPartition(int[] nums) {
//zhuangtai, hang
int len = nums.length;
int total = 0;
for(int num: nums) total += num;
if(total % 2 != 0) return false;
boolean [][] dp = new boolean [len][total /2 + 1];
if(nums[0]<total/2) dp[0][nums[0]] = true;
for(int i = 1;i<len;i++){
for(int j= 0;j<=total/2;j++){
if(nums[i] == j){
dp[i][j] = true;
continue;
}
if(nums[i] <j){
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
}
}
}
return dp[len-1][total/2];
}
搜索问题都是这样做的,有一个map, 走过的点全都打上标记。遇到标记了就说明不能走,也就是每次走没有标记的位置(可能还有些别的条件,像这里),所以主要是遍历的方式,每一次要做啥,做完去哪里?那里走到头发现不通,咋回去?
对于 决策问题 也可以这样做,相当于搜索的是一棵树了。
class Solution {
//要想不相互攻击,每行每列每个斜着的线都只能有一个皇后
List<List<String>> res = new LinkedList<>();
public List<List<String>> solveNQueens(int n) {
char[][] maps = new char[n][n];
for(char[] map : maps) Arrays.fill(map,'.');
LinkedList<String> cur = new LinkedList<String>();
dfs(maps, 0, cur);
return res;
}
public void dfs(char[][] maps, int row,LinkedList<String> cur){
if(row == maps.length ){
res. add(new LinkedList<>(cur));
return;
}
int n = maps[row].length;
for(int i =0; i< n;i++){
if(!isValid(maps, row, i)){
continue;
}
maps[row][i] = 'Q';
cur.add(new String(maps[row]));
dfs(maps, row+1,cur);
maps[row][i] = '.';
cur.removeLast();
}
}
public boolean isValid(char[][] maps, int row,int col){
//检查这一列,以及这个点的左上和右上,因为是遍历着往下填,所以不需要考虑对角线的下半部分
//列
for(int i =0;i<maps.length;i++){
if(maps[i][col] == 'Q') {
return false;
}
}
//右上方
for(int i = row-1, j = col+1; i>=0 && j<maps[row].length;i--,j++){
if(maps[i][j] == 'Q'){
return false;
}
}
//左上方
for(int i = row-1, j= col-1;i>=0 && j>=0; i--,j--){
if(maps[i][j] == 'Q'){
return false;
}
}
return true;
}
}
class Solution {
LinkedList<String> res = new LinkedList<String>();
public String[] permutation(String S) {
//先思考一下,读懂题目的隐藏条件才行
//有重复 意味着 如果直接搜索列举排列所有的字符串,会有重复的,
//所以 得剪枝,方法跟之前的相同, 剪掉的是重复的,所以 得先把 字符串 按字典序 排序
char[] s = S.toCharArray();
Arrays.sort(s);
boolean[] used = new boolean[s.length];
dfs(s, 0, new StringBuilder(),used );
//注意list转String的方式,直接toArray得到的是object类型的
return res.toArray(new String[res.size()]);
}
public void dfs(char[] s, int index, StringBuilder cur,boolean[] used ){
if(cur.length() == s.length){
res.add(cur.toString());
return ;
}
for(int i = 0; i<s.length; i++){
if(!used[i]){
if(i>0 && s[i] == s[i-1] && !used[i-1] ){
continue;
}
cur.append(s[i]);
used[i] = true;
dfs(s, i+1, cur,used);
cur.deleteCharAt(cur.length()-1);
used[i] = false;
}
}
}
}
class Solution {
//硬币是可以重复使用的
//一下就想到了做法,却在写代码时卡了几次
public int coinChange(int[] coins, int amount) {
if(coins ==null||coins.length ==0) return -1;
//各个和所需要的最小硬币数目
int[] dp = new int[amount+1];
//注意这里没必要全都填最大整数,只要填amount+1,除非全用1 ,这是最小的情况了
Arrays.fill(dp, amount+1);
dp[0] = 0;
for(int i=1;i<=amount;i++){
for(int j = 0;j< coins.length;j++){
if(i-coins[j] >=0 && dp[i-coins[j]] != amount+1){
dp[i] = Math.min(dp[i],dp[i-coins[j]]+1);
}
}
}
return dp[amount] == amount+1 ? -1: dp[amount];
}
//dfs
//硬币是可以重复使用的
//dfs想法也很自然的,就是每次选一个数,下一次递归时就递归一个当前需要减掉选的这个数
//dfs只能写void,不能写带返回值的,因为没剪枝,所以会超时,相当于暴力递归了
int min = Integer.MAX_VALUE;
public int coinChange(int[] coins, int amount) {
if(coins==null||coins.length==0) return -1 ;
dfs(coins, amount, 0);
return min == Integer.MAX_VALUE? -1:min;
}
public void dfs( int[] coins, int amount, int res){
if(amount < 0) return ;
if(amount == 0){
min = Math.min(res, min);
return ;
}
for(int i=0;i<coins.length;i++){
dfs(coins,amount-coins[i],res+1);
}
}
}
要尽量减少 dfs的次数,一开始没有及时返回,没有进行判断,会超时
class Solution {
//DFS,
// 矩阵中每个点都可以作为开始结点
// 用过的点不能再用,所以要加标记,还要在回溯回来的过程中取消标记
//超时怎么办,就尽量减少递归次数
boolean res = false;
public boolean exist(char[][] board, String word) {
if(board == null || word == null) return false;
char[] chars = word.toCharArray();
int len = chars.length;
int m = board.length;
int n = board[0].length;
int[][] used = new int[m][n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
//不需要所有的都遍历,只要找到一个true就可以立即返回
if(board[i][j] == chars[0]){
dfs(board,chars,0,i,j,used);
}
if(res){
return true;
}
}
}
return false;
}
public void dfs(char[][] board, char[] chars, int index,int i, int j,int[][] used){
if(index == chars.length){
res = true;
return ;
}
if(i<0 || i>board.length-1|| j<0 || j>board[0].length-1) return ;
if(used[i][j] == 1) return ;
if(board[i][j] != chars[index]) return ;
used[i][j] = 1;
//这里也要随时判断来减少递归的次数
dfs(board,chars,index+1,i+1,j,used);
if(res) return;
dfs(board,chars,index+1,i-1,j,used);
if(res) return;
dfs(board,chars,index+1,i,j+1,used);
if(res) return;
dfs(board,chars,index+1,i,j-1,used);
if(res) return;
used[i][j] = 0;
}
}
判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
这题的点有两个,一是递归时是要知道左右子树的高度的
二是:如何融合既需要高度又需boolean的递归;
public boolean isBalanced(TreeNode root) {
return dfs(root)==-1? false:true;
}
public int dfs(TreeNode root){
if(root == null) return 0;
int left = dfs(root.left);
if(left == -1) return -1;
int right = dfs(root.right);
if(right == -1) return -1;
return Math.abs(left-right) >1?-1 : Math.max(left,right)+1;
}
class Solution {
LinkedList<String> res = new LinkedList<String>();
public String[] permutation(String S) {
//先思考一下,读懂题目的隐藏条件才行
//有重复 意味着 如果直接搜索列举排列所有的字符串,会有重复的,
//所以 得剪枝,方法跟之前的相同, 剪掉的是重复的,所以 得先把 字符串 按字典序 排序
char[] s = S.toCharArray();
Arrays.sort(s);
boolean[] used = new boolean[s.length];
dfs(s, 0, new StringBuilder(),used );
//注意list转String的方式,直接toArray得到的是object类型的
return res.toArray(new String[res.size()]);
}
public void dfs(char[] s, int index, StringBuilder cur,boolean[] used ){
if(cur.length() == s.length){
res.add(cur.toString());
return ;
}
for(int i = 0; i<s.length; i++){
if(!used[i]){
if(i>0 && s[i] == s[i-1] && !used[i-1] ){
continue;
}
cur.append(s[i]);
used[i] = true;
dfs(s, i+1, cur,used);
cur.deleteCharAt(cur.length()-1);
used[i] = false;
}
}
}
}
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
class Solution {
//一个经典的回溯,枚举所有的可能的分割情况
//不合理的直接返回, 合理的直到分割了四个数了,就加进去
//思路好懂,但是代码不好写,
// 难在 一: 合理怎么判断
//二:对于每个位置的数,要考虑它下一步的情况。。应该把哪些列为下一步要考虑的???
//对于每一个数字,最多往后考虑三位,如果已有三位,要加点,,,只有一位的也可以加点啊,还要考虑0开头不对
//但其实这样只会增加代码的实现的复杂度, 可以直接往后,组成的数字不合理就停
List<String> res = new LinkedList<String>();
int SegCount = 4;
int[] segments = new int[SegCount];
public List<String> restoreIpAddresses(String s) {
int len = s.length();
if(len < 4 || len > 12) return new ArrayList<String>();
dfs(s, 0, 0);
return res;
}
public void dfs(String s, int cur, int segmentId){
//合理的要加进去的情况,数组满且字符串用完
if(segmentId == SegCount){
if(cur == s.length()){
StringBuilder sb = new StringBuilder();
for(int i = 0; i < SegCount; i++){
sb.append(segments[i]);
if(i != SegCount-1){
sb.append('.');
}
}
res.add(sb.toString());
}
return;
}
//虽然数组没填满,但字符串用完了的情况
if(cur == s.length()){
return;
}
//开始为0的情况
if(s.charAt(cur) == '0'){
segments[segmentId] = 0;
dfs(s, cur+1, segmentId+1);
}
//普通的情况,就是以当前坐标往后尽可能的弄各种数字
int countNum = 0;
for(int i = cur; i < s.length(); i++){
countNum = countNum * 10 + (s.charAt(i) - '0');
//countNum只要是合理的,就可以继续往下递归
if(countNum > 0 && countNum <= 0xFF){
segments[segmentId] = countNum;
dfs(s, i+1, segmentId+1);
}else{
break;
}
}
}
}
动态规划算法是一种空间换时间的策略。我的理解本质上动态规划还是一个决策过程,每一步都会有选择,并且这个选择会对后面的结果产生影响。满足无后效性和最优子结构。
无后效型:如果给定 某一状态,则在这一阶段以后的发展过程 不受 这一状态之前的各种状态的影响。
最优子结构:大问题的最优解可以由小问题的最优解推出。
问题引入
自上而下分析DP
DP问题的核心是状态转移方程,如何得到这个方程呢?
可以从结果状态出发,看结果状态是如何由前一个或前几个状态得到。(做题时注意分析)。一般需要分类讨论各种可能出现的情况。
DP实施
在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。也就是说仍然从头开始考虑所有的可能性,但是只保留那部分有可能成为最优解的。
考虑DP问题的步骤:
状态 ,一般是所求是什么,什么就是状态;
状态转移方程 ,自上而下分析;
状态初始化 ,可以直接得到的状态,或者状态转移方程的边界值;
输出,返回值,状态转移矩阵出口处理;
考虑是否能进行状态压缩,即压缩状态转移矩阵。
最终解决DP问题需要按照状态转移方程,从初始化的状态开始打表格。
必要时从状态转移方程中看是否需要多设置一行或者一列哨兵,可以避免很多边界值的讨论。
Given a target find minimum (maximum) cost / path / sum to reach the target.
routes[i] = min(routes[i-1], routes[i-2], … , routes[i-k]) + cost[i];所以可以到达当前状态的值中最小的加上这个状态的值。(参考上面的Q点)
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
1 状态:到达当前位置的最大连续子数组和;
2 状态转移: 对于任一个状态,要么等于前面的最大和加它本身,要么等于它自己,
//dp[0]初始化为nums[0]的值,或者最小int
for(int i=1;i<nums.length;i++){
dp[i] = Math.max(dp[i-1]+ nums[i],nums[i]);
res = dp[i] > res? dp[i]:res;
}
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
每次只能向下或者向右移动一步。
最暴力的做法,就是搜索出所有的左上到右下的路径,并求路径和最小的那个(DFS)
二维矩阵的DP
DP矩阵为一个二维数组,从左上角开始填充,出口为右下角。
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
if (m == 0 || n == 0) {
return 0;
}
// 初始化
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
//也算初始化,因为最上面的一行只能从左边得到
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
//最左边的一列只能从上面得到
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
//dp过程
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
//出口
return dp[m - 1][n - 1];
}
优化
不使用额外的空间,直接在原矩阵上修改。因为dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j],前面的值修改后后面不会再使用到。
public int minPathSum(int[][] grid) {
for(int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[0].length; j++) {
if(i == 0 && j == 0) continue;
else if(i == 0) grid[i][j] = grid[i][j - 1] + grid[i][j];
else if(j == 0) grid[i][j] = grid[i - 1][j] + grid[i][j];
else grid[i][j] = Math.min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
}
}
return grid[grid.length - 1][grid[0].length - 1];
}
剑指 Offer 55 - II. 平衡二叉树
判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
这题的点有两个,一是递归时是要知道左右子树的高度的
二是:如何融合既需要高度又需boolean的递归;
public boolean isBalanced(TreeNode root) {
return dfs(root)==-1? false:true;
}
public int dfs(TreeNode root){
if(root == null) return 0;
int left = dfs(root.left);
if(left == -1) return -1;
int right = dfs(root.right);
if(right == -1) return -1;
return Math.abs(left-right) >1?-1 : Math.max(left,right)+1;
}
class Solution {
LinkedList<String> res = new LinkedList<String>();
public String[] permutation(String S) {
//先思考一下,读懂题目的隐藏条件才行
//有重复 意味着 如果直接搜索列举排列所有的字符串,会有重复的,
//所以 得剪枝,方法跟之前的相同, 剪掉的是重复的,所以 得先把 字符串 按字典序 排序
char[] s = S.toCharArray();
Arrays.sort(s);
boolean[] used = new boolean[s.length];
dfs(s, 0, new StringBuilder(),used );
//注意list转String的方式,直接toArray得到的是object类型的
return res.toArray(new String[res.size()]);
}
public void dfs(char[] s, int index, StringBuilder cur,boolean[] used ){
if(cur.length() == s.length){
res.add(cur.toString());
return ;
}
for(int i = 0; i<s.length; i++){
if(!used[i]){
if(i>0 && s[i] == s[i-1] && !used[i-1] ){
continue;
}
cur.append(s[i]);
used[i] = true;
dfs(s, i+1, cur,used);
cur.deleteCharAt(cur.length()-1);
used[i] = false;
}
}
}
}
DP问题的核心是状态转移方程,如何得到这个方程呢?
可以从结果状态出发,看结果状态是如何由前一个或前几个状态得到。(做题时注意分析)。一般需要分类讨论各种可能出现的情况。
在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。也就是说仍然从头开始考虑所有的可能性,但是只保留那部分有可能成为最优解的。
考虑DP问题的步骤:
Given a target find minimum (maximum) cost / path / sum to reach the target.
routes[i] = min(routes[i-1], routes[i-2], … , routes[i-k]) + cost[i];所以可以到达当前状态的值中最小的加上这个状态的值。(参考上面的Q点)
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
1 状态:到达当前位置的最大连续子数组和;
2 状态转移: 对于任一个状态,要么等于前面的最大和加它本身,要么等于它自己,
//dp[0]初始化为nums[0]的值,或者最小int
for(int i=1;i<nums.length;i++){
dp[i] = Math.max(dp[i-1]+ nums[i],nums[i]);
res = dp[i] > res? dp[i]:res;
}
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
每次只能向下或者向右移动一步。
最暴力的做法,就是搜索出所有的左上到右下的路径,并求路径和最小的那个(DFS)
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
if (m == 0 || n == 0) {
return 0;
}
// 初始化
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
//也算初始化,因为最上面的一行只能从左边得到
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
//最左边的一列只能从上面得到
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
//dp过程
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
//出口
return dp[m - 1][n - 1];
}
public int minPathSum(int[][] grid) {
for(int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[0].length; j++) {
if(i == 0 && j == 0) continue;
else if(i == 0) grid[i][j] = grid[i][j - 1] + grid[i][j];
else if(j == 0) grid[i][j] = grid[i - 1][j] + grid[i][j];
else grid[i][j] = Math.min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
}
}
return grid[grid.length - 1][grid[0].length - 1];
}
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
public int minimumTotal(List<List<Integer>> triangle) {
if(triangle == null || triangle.size()==0) return 0;
int m = triangle.size();
int n = triangle.get(m-1).size();
//dp 矩阵,状态就是每个位置对应的最小路径值
int[][] dp = new int[m][n];
//初始化
dp[0][0] = triangle.get(0).get(0);
int res = Integer.MAX_VALUE;
//遍历,注意是三角形,每一列的数等于行号
for(int i = 1; i< m; i++){
for(int j = 0; j <= i; j++){
//先处理两种特殊情况
if(j == 0){
dp[i][j] = dp[i-1][j] + triangle.get(i).get(j);
}else if(j == i){
dp[i][j] = dp[i-1][j-1] + triangle.get(i).get(j);
}else{
dp[i][j] = Math.min(dp[i-1][j],dp[i-1][j-1]) + triangle.get(i).get(j);
}
}
}
//出口
for(int i= 0;i < n;i++){
res = res<dp[m-1][i] ? res:dp[m-1][i];
}
return res;
}
空间优化,使用o(n)的空间复杂度。也就是二维dp数组压缩成一维的过程。因为更新每个值时需要用到上一行的两个值,所以需要两个临时变量来存。
int prev = 0, cur;
for (int i = 1; i < triangle.size(); i++) {
//对每一行的元素进行推导
List<Integer> rows = triangle.get(i);
for (int j = 0; j <= i; j++) {
cur = dp[j];
if (j == 0) {
// 最左端特殊处理
dp[j] = cur + rows.get(j);
} else if (j == i) {
// 最右端特殊处理
dp[j] = prev + rows.get(j);
} else {
dp[j] = Math.min(cur, prev) + rows.get(j);
}
prev = cur;
}
}
152. 乘积最大子数组
在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积(好像那个找有个岛的问题哦,不过那个是dfs)。
首先 ,可以有暴力方法,每增加一行或一列判断值是否全为1。
动态规划:dp(i,j) 表示以 (i, j) 为右下角,且只包含 11 的正方形的边长最大值。动态规划中,当前状态要由前面的状态确定,此时需要考虑三个状态,而不是仅仅考虑对角线的,详情可以看这个。
public int maximalSquare(char[][] matrix) {
if(matrix == null || matrix.length==0 ||matrix[0].length==0) return 0;
int res = 0;
// 注意状态是什么
int row = matrix.length;
int col = matrix[0].length;
int[][] dp = new int[row][col];
//初始化
for(int i = 0;i<col;i++) {
dp[0][i] = matrix[0][i] ==1 ? 1:0;
}
for(int j = 0;j<row;j++){
dp[j][0] = matrix[j][0]==1 ? 1:0;
}
for(int i = 1;i<row;i++){
for(int j = 1;j<col;j++){
if(matrix[i][j] =='1'){
dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i-1][j-1],dp[i][j-1])) +1;
}
res = Math.max(res,dp[i][j]);
}
}
//状态是以该点为右下角的最大的边长,返回值应该平方
return res*res;
}
279. 完全平方数
这题不太明白,贪心的做法也还没看
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
很直接的思路是暴力枚举所有比n小的完全平方数,并枚举所有的组合选小的,但是这题没给n的范围,也就是会有n很大的情况,暴力会超时。
对于n,考虑所有的n-k的情况,k为小于n的完全平方数,这样就变成了递归:numSquares(n) = min(numSquares(n-k) + 1) ∀k∈square numbers
但仍然有超时的问题,因为这个过程存在着大量的重复计算。因此可以考虑使用DP算法。
需要提前计算所有小于等于n的完全平方数。
状态:1-n 每个的数的最小 个数。
public int numSquares(int n) {
int dp[] = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
// bottom case
dp[0] = 0;
// pre-calculate the square numbers.
int max_square_index = (int) Math.sqrt(n) + 1;
int square_nums[] = new int[max_square_index];
for (int i = 1; i < max_square_index; ++i) {
square_nums[i] = i * i;
}
for (int i = 1; i <= n; ++i) {
for (int s = 1; s < max_square_index; ++s) {
if (i < square_nums[s])
break;
dp[i] = Math.min(dp[i], dp[i - square_nums[s]] + 1);
}
}
return dp[n];
}
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
public int coinChange(int[] coins, int amount) {
//状态
int[] dp = new int[amount+1];
Arrays.fill(dp, amount+1);
//初始化
dp[0] = 0;
//dp过程
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++){
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
//出口为什么是这个?
return dp[amount] > amount? -1:dp[amount];
}
Given a target find a number of distinct ways to reach the target.
解法:Sum all possible ways to reach the current state.
routes[i] = routes[i-1] + routes[i-2], … , + routes[i-k]
Generate sum for all values in the target and return the value for the target.
for (int i = 1; i <= target; ++i) {
for (int j = 0; j < ways.size(); ++j) {
if (ways[j] <= i) {
dp[i] += dp[i - ways[j]];
}
}
}
return dp[target]
递归解法会超时!!!
注意考虑边界n和不符合要求的n
public int climbStairs(int n) {
if(n<2) return n;
//1 什么是状态,n个台阶,爬到每个台阶时的方法数
int[] dp = new int[n];
//2 状态初始化
dp[0] = 1;
dp[1] = 2;
//3 状态转移
for(int i =2;i<n;i++){
dp[i] = dp[i-1] + dp[i-2];
}
//dp出口
return dp[n-1];
}
这个题有点像T64,只不过64还要计算最小路径和。
public int uniquePaths(int m, int n) {
//状态
int[][] dp = new int[n][m];
//初始化,只能向下向右,所以第一行第一列可以初始化
for(int i = 0;i<n; i++){
dp[i][0] = 1;
}
for(int j =0; j<m; j++){
dp[0][j] = 1;
}
//dp 过程
for(int i = 1;i<n ;i++){
for(int j = 1;j<m;j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
//出口
return dp[n-1][m-1];
}
T62只能向右或向下走,这题不仅向下向右,还添加了障碍物。
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
//状态
int[][] dp = new int[m][n];
//初始化
boolean flag = false;
for(int i=0;i<n;i++){
if(obstacleGrid[0][i]==1) flag = true;
if(flag==true) dp[0][i] = 0;
else dp[0][i] = 1;
}
flag = false;
for(int i=0;i<m;i++){
if(obstacleGrid[i][0]==1) flag = true;
if(flag==true) dp[i][0] = 0;
else dp[i][0] = 1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(obstacleGrid[i][j] == 1) dp[i][j] = 0;
else dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
这题直接想用回溯做,但是回溯的话肯定会有重复的问题,所以必须要剪枝。既然有重复,可以考虑记忆化搜索,也就是DP。
public int combinationSum4(int[] nums, int target) {
//状态,就是题目中说的 和为目标正整数的组合的数目,目标从1到n;
int[] dp = new int[target+1];
//初始化,这里的dp[0]不是 目标是0,而是一个标志,因为用到0的时候,说明nums中出现过当前这个目标数字
//所以设置为1
dp[0] = 1;
//
for(int i =1;i<=target;i++){
for(int num : nums){
if(i-num >= 0){
dp[i] += dp[i-num];
}
}
}
return dp[target];
}
更细节上来说,这是一种背包问题:
常见的背包问题有1、组合问题。2、True、False问题。3、最大最小问题。分为三类。
1、组合问题:377. 组合总和 Ⅳ, 494. 目标和 , 518. 零钱兑换 II
2、True、False问题:139. 单词拆分 416. 分割等和子集
3、最大最小问题: 474. 一和零 322. 零钱兑换
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
public int findTargetSumWays(int[] nums, int S) {
//状态 dp[i][j] 表示用数组中的前 i 个元素,组成和为 j 的方案数
int[][] dp = new int[nums.length][2001];
dp[0][nums[0] + 1000] = 1;
dp[0][-nums[0] + 1000] += 1;
for (int i = 1; i < nums.length; i++) {
for (int sum = -1000; sum <= 1000; sum++) {
if (dp[i - 1][sum + 1000] > 0) {
dp[i][sum + nums[i] + 1000] += dp[i - 1][sum + 1000];
dp[i][sum - nums[i] + 1000] += dp[i - 1][sum + 1000];
}
}
}
return S > 1000 ? 0 : dp[nums.length - 1][S + 1000];
}
300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
public int lengthOfLIS(int[] nums) {
int n = nums.length;
//状态,以数组中每个位置为结尾的子串的 最长上升子序列
int[] dp = new int[n];
//初始化
Arrays.fill(dp, 1);
for(int i=1;i<n;i++){
for(int j=0; j<i; j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j]+1);
}
}
}
int res = 0;
for(int i=0; i<n;i++){
res = Math.max(dp[i],res);
}
return res;
}
674 最长连续递增序列
给定一个未经排序的整数数组,找到最长且连续的的递增序列。
滑动窗口 和 动态规划 都可以做。
滑动窗口做法:
public int findLengthOfLCIS(int[] nums) {
int ans = 0, anchor = 0;
for (int i = 0; i < nums.length; ++i) {
if (i > 0 && nums[i-1] >= nums[i]) anchor = i;
ans = Math.max(ans, i - anchor + 1);
}
return ans;
}
DP做法:
public int findLengthOfLCIS(int[] nums) {
if(nums==null || nums.length==0) return 0;
//状态
int[] dp = new int[nums.length];
//初始化
dp[0] = 1;
for(int i=1; i< nums.length;i++){
if(nums[i] > nums[i-1]){
dp[i] = dp[i-1]+1;
}else{
dp[i] = 1;
}
}
int res = 0;
for(int i=0;i<nums.length;i++){
res = dp[i]>res?dp[i]:res;
}
return res;
}
T300和T674 可以对比得到连续和非连续的区别,不连续的要对位置i 前面所有的进行判断,而连续的只考虑前一个。
673. 最长递增子序列的个数
给定一个未排序的整数数组,找到最长递增子序列的个数。
???充满疑问的一道题
128 最长连续序列
给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。
这题可以dp,但是排序后顺着找就可以,没必要dp呀。
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) return 0;
Arrays.sort(nums);
int n = nums.length;
int max = 1, cur = 1;
for (int i = 1; i < n; i++) {
if (nums[i] != nums[i - 1]) {
if (nums[i - 1] + 1 == nums[i]) cur++;
else {
max = Math.max(max, cur);
cur = 1;
}
}
}
return Math.max(max, cur);
}
Given a set of numbers find an optimal solution for a problem considering the current number and the best you can get from the left and right sides.
Get the best from the left and right sides and add a solution for the current
position.for(int l = 1; l
96. 不同的二叉搜索树
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
给定一个有序序列 1 … n,为了根据序列构建一棵二叉搜索树。我们可以遍历每个数字 i,将该数字作为树根,1 … (i-1) 序列将成为左子树,(i+1) … n 序列将成为右子树。于是,我们可以递归地从子序列构建子树(也就是区间dp)。
G(n): 长度为n的序列的不同二叉搜索树个数。
F(i, n): 以i为根的不同二叉搜索树个数。
对于根 i 的不同二叉搜索树数量 F(i, n)F(i,n),是左右子树个数的笛卡尔积(组合)
public int numTrees(int n) {
int[] G = new int[n + 1];
G[0] = 1;
G[1] = 1;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
//合并了两个公式
G[i] += G[j - 1] * G[i - j];
}
}
return G[n];
}
1130. 叶值的最小代价生成树
public int mctFromLeafValues(int[] arr) {
int n = arr.length;
//这道题的核心是:
//要知道中序遍历就决定了arr数组(0...n-1)里的第k位元素的所有左边元素(包括它自己)都在左子树里,
//而其右边元素都在右子树里
//而此时左右两边子树分别选出最大值的乘积就是此时的根,也就是题目中说的非叶节点
//所以我们可以假定从i到j位,最小和可能是:此刻k位左右两边元素中最大值的乘积 + 子问题k左边(i,k)的最小值
// + 子问题k位右边(k+1,j)的最小值
//即:dp[i][j]=min(dp[i][j], dp[i][k] + dp[k+1][j] + max[i][k]*max[k+1][j])
//这道题跟leetcode1039一个套路
//求arr从i到j之间的元素最大值, 保存在max[i][j]中
//这道题i和j是可以相等的
int[][] max = new int[n][n];
for (int j=0;j<n;j++) {
int maxValue = arr[j];
for (int i=j;i>=0;i--) {
maxValue = Math.max(maxValue, arr[i]);
max[i][j] = maxValue;
}
}
int[][] dp = new int[n][n];
for (int j=0; j<n; j++) {
for (int i=j; i>=0; i--) {
//k是i到j之间的中间某个值,i<=k
int min = Integer.MAX_VALUE;
for (int k=i; k+1<=j; k++) {
min = Math.min(min,dp[i][k] + dp[k+1][j] + max[i][k]*max[k+1][j]);
dp[i][j] = min;
}
}
}
return dp[0][n-1];
}
== 312. 戳气球==
Given two strings s1 and s2, return some result.
Most of the problems on this pattern requires a solution that can be accepted in O(n^2) complexity.
// i - indexing string s1
// j - indexing string s2
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (s1[i-1] == s2[j-1]) {
dp[i][j] = /*code*/;
} else {
dp[i][j] = /*code*/;
}
}
}
If you are given one string s the approach may little vary.
for (int l = 1; l < n; ++l) {
for (int i = 0; i < n-l; ++i) {
int j = i + l;
if (s[i] == s[j]) {
dp[i][j] = /*code*/;
} else {
dp[i][j] = /*code*/;
}
}
}
解码方法
class Solution {
//这题倒是想到了动态方程,但是分类讨论情况太多,看看人家是怎么整合的
//情况很多的时候与其考虑去掉不可以的,不如直接考虑能用的,剩下的一起处理
//char转int ,char-'0'
//每个数字,要么自己作为一个数字 映射到一个字符,要么和它前面的数字组合成一个1到26 的数字
public int numDecodings(String s) {
if(s == null || s.length()==0) return 0;
if(s.charAt(0)=='0') return 0;
int len =s.length();
int[] dp = new int[len+1];
dp[0] = 1;//初始化没明白
dp[1] = 1;
s = " " +s;
for(int i=2;i<=len;i++){
//
//if(s.charAt(i)<1||s.charAt(i)>)
boolean flag = false;
// 先考虑单个数的qingk
if(s.charAt(i)!='0') {
dp[i] += dp[i-1];
flag =true;
}
//进一步考虑
if(s.charAt(i-1)!='0' && (s.charAt(i-1)-'0')* 10+ s.charAt(i)-'0' <=26 && (s.charAt(i-1)-'0')* 10+ s.charAt(i)-'0' > 0 ){
dp[i] += dp[i-2];
flag = true;
}
//两种都不满足就是不符合要求
if(!flag) return 0;
}
return dp[len];
}
}
1143 最长公共子序列 ⭐️
大部分比较困难的字符串问题都和这个问题一个套路
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
状态:二维DP数组,dp[i][j] 表示第一个字符串到 i 位置,第二个字符串到 j 位置的, 最长公共子串
初始化:让索引为 0 的行和列表示空串,dp[0][…] 和 dp[…][0] 都应该初始化为 0;
public int longestCommonSubsequence(String text1, String text2) {
//状态,以当前位置为结尾的两个子串的最长公共子序列的长度
int len1 = text1.length();
int len2 = text2.length();
//一般为了考虑空串,多加一行一列
int[][] dp = new int[len1+1][len2+1];
//初始化
for(int i=0;i<=len1;i++){
dp[i][0] = 0;
}
for(int i=0; i<=len2;i++){
dp[0][i] = 0;
}
//注意这里的dp转移,子序列问题的状态转移都差不多
for(int i=1;i <= len1; i++){
for(int j=1; j<= len2; j++){
if(text1.charAt(i-1) == text2.charAt(j-1))
dp[i][j] = dp[i-1][j-1] + 1;
else
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
return dp[len1][len2];
}
72 编辑距离⭐️
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
// 有一个字符串为空串
if (n * m == 0)
return n + m;
//状态,word1的i位置到word2的j位置的最小次数
int[][] dp = new int[n+1][m+1];
for(int i=0;i<= n;i++){
dp[i][0] = i;
}
for(int i=0;i<=m;i++){
dp[0][i] = i;
}
//dp[i][j-1]替换A串i位置,dp[i-1][j]替换B串j位置,
//要么就在dp[i-1][j-1]的基础上考虑,若相等就不用再修改了
for(int i = 1;i<=n;i++){
for(int j = 1;j<=m;j++){
int tmp = dp[i-1][j-1];
if(word1.charAt(i-1)!= word2.charAt(j-1))
tmp = tmp+1;
dp[i][j] = Math.min(dp[i][j-1]+1,Math.min(dp[i-1][j]+1,tmp));
}
}
return dp[n][m];
}
10. 正则表达式匹配
115 不同的子序列
给定一个字符串 S 和一个字符串 T,计算在 S 的子序列中 T 出现的个数。
当 S[j] == T[i] , dp[i][j] = dp[i-1][j-1] + dp[i][j-1] :
有两种情况
匹配时最后一个字符为s[j](那么我们前j-1个字符只需要匹配T的前j-1个字符就行了,即dp[i-1][j-1])
或者不是(那么由s[j-1]前面出现过的T[i]作为最后一个字符了,即dp[i][j-1])
状态转移方程没搞懂
public int numDistinct(String s, String t) {
int[][] dp = new int[t.length() + 1][s.length() + 1];
for (int j = 0; j < s.length() + 1; j++) dp[0][j] = 1;
for (int i = 1; i < t.length() + 1; i++) {
for (int j = 1; j < s.length() + 1; j++) {
if (t.charAt(i - 1) == s.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1];
else dp[i][j] = dp[i][j - 1];
}
}
return dp[t.length()][s.length()];
}
1092. 最短公共超序列
712. 两个字符串的最小ASCII删除和
647. 回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。
public int countSubstrings(String s) {
if(s == null || s.equals("")){
return 0;
}
int n = s.length();
int res = n;
//状态 从i到j是不是回文串,所以只填上三角矩阵即可
boolean[][] dp = new boolean[n][n];
//初始化
for(int i = 0;i<n;i++) dp[i][i] = true;
//dp[i][j] = dp[i+1][j-1];所以从右下角开始更新
for(int i= n-1; i>=0;i--){
for(int j = i+1;j<n;j++){
if(s.charAt(i)==s.charAt(j)){
//特殊情况,因为直接用状态可能会有得不到偶数中心的情况
if(j-i==1) dp[i][j] = true;
else dp[i][j] = dp[i+1][j-1];
}else{
dp[i][j] = false;
}
if(dp[i][j]) res++;
}
}
return res;
}
5 最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
DP做法跟647很像
//像673一样,找到所有的回文子串,并在这个过程中更新最大子串的长度,以及开始和结束的位置
public String longestPalindrome(String s) {
if(s==null || s.length() < 2) return s;
int n = s.length();
boolean[][] dp = new boolean[n][n];
int max_len = 1;
int start = 0, end = 0;
//初始化
for(int i = 0;i<n;i++) dp[i][i] = true;
for(int i = n-1;i>=0;i--){
for(int j = i+1;j< n;j++){
if(s.charAt(i)==s.charAt(j)){
if(j-i==1) dp[i][j] = true;
else dp[i][j] = dp[i+1][j-1];
}else{
dp[i][j] = false;
}
if(dp[i][j] && j-i+1 > max_len){
max_len = j-i+1;
start = i;
end = j;
}
}
}
return s.substring(start,end+1);
}
516 最长回文子序列
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
public int longestPalindromeSubseq(String s) {
//跟子串做法相似
if(s == null) return 0;
int n = s.length();
if(n==1) return 1;
int[][] dp = new int[n][n];
for(int i = 0;i<n;i++) dp[i][i] = 1;
int res = 0;
for(int i=n-1;i>=0;i--){
for(int j = i+1;j<n;j++){
if(s.charAt(i)==s.charAt(j)){
dp[i][j] = dp[i+1][j-1]+2;
}else{
//????
dp[i][j] = Math.max
(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][n-1];
}
Given a set of values find an answer with an option to choose or ignore the current value.
// i - indexing a set of values
// j - options to ignore j values
for (int i = 1; i < n; ++i) {
for (int j = 1; j <= k; ++j) {
dp[i][j] = max({dp[i][j], dp[i-1][j] + arr[i], dp[i-1][j-1]});
dp[i][j-1] = max({dp[i][j-1], dp[i-1][j-1] + arr[i], arr[i]});
}
}
198 打家劫舍
public int rob(int[] nums) {
if(nums==null||nums.length==0) return 0;
int n = nums.length;
//状态
int[] dp = new int[n];
dp[0] = nums[0];
if(n>1) dp[1] = Math.max(nums[1],nums[0]);
for(int i = 2;i<n;i++){
dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[n-1];
}
213. 打家劫舍 II
与198的区别在于,这里加了一个条件,房子是首尾相连的。
可以看成是两个单列, 一个第一个置为0,另一个最后一个置为0,取最大的。
public int rob(int[] nums) {
if(nums==null||nums.length==0) return 0;
if(nums.length==1) return nums[0];
//左闭右开哦
return Math.max(robrange(Arrays.copyOfRange(nums, 0, nums.length - 1)),
robrange(Arrays.copyOfRange(nums,1,nums.length)));
}
public int robrange(int[] nums){
int pre=0,cur =0,tmp;
for(int num:nums){
tmp = cur;
cur = Math.max(pre + num, cur);
pre = tmp;
}
return cur;
}
打家劫舍3
class Solution {
//什么叫 两个直接相连的房子在 同一天 晚上 被打劫 ,这句话可以转化成什么?
//选了根节点,两个子节点就都不能选了,选了子节点, 子节点的字节点又不能选了
//有点像DP感觉,按层遍历即可
//但是又是二叉树,是不是可以按DFS来做???(觉得DFS不可)
//最终: DFS,后序优先遍历, DP
//DP的状态方程也有可能是依赖于后面的状态,那就从后往前遍历就行了,数组是从大下标到小下标,树是后序遍历
//树型DP,遍历的过程不同
//这题还有点像那个最大乘积,就是得用两个DP数组
public int rob(TreeNode root) {
//考虑某一个点,选它就等于 左右都不选的最大值,不选它就等于 左右选不选都有可能的最大值
Map<TreeNode, Integer> f = new HashMap<TreeNode,Integer>();
Map<TreeNode, Integer> g = new HashMap<TreeNode,Integer>();
dfs(root,f,g);
return Math.max(f.getOrDefault(root,0) ,g.getOrDefault(root,0));
}
public void dfs(TreeNode root, Map<TreeNode,Integer> f,Map<TreeNode,Integer> g){
if(root ==null) return ;
dfs(root.left,f,g);
dfs(root.right,f,g);
f.put(root, root.val + g.getOrDefault(root.left,0) + g.getOrDefault(root.right,0));
g.put(root, Math.max(f.getOrDefault(root.left,0),g.getOrDefault(root.left,0))+ Math.max(f.getOrDefault(root.right,0),g.getOrDefault(root.right,0)));
}
}
121. 买卖股票的最佳时机
解法一:
对每一个位置,记录当前位置对应的最低价,然后计算赚的钱数。
public int maxProfit(int[] prices) {
if(prices==null || prices.length==0) return 0;
int min = Integer.MAX_VALUE;
int maxprofit = 0;
for(int i = 0;i<prices.length;i++){
if(prices[i]>min){
if((prices[i]-min)>maxprofit) maxprofit = prices[i]-min;
}else{
min = prices[i];
}
}
return maxprofit;
}
解法二 动态规划
状态 dp[i][j] 表示:在索引为 i 的这一天,用户手上持股状态为 j 所获得的最大利润;
dp[0][1] 代表购入了股票,dp[0][0] 代表买入又卖出; 状态 0 特指:“卖出股票以后不持有股票的状态”,请注意这个状态和“没有进行过任何一次交易的不持有股票的状态”的区别
public int maxProfit(int[] prices) {
if(prices==null || prices.length==0) return 0;
//每一天持股和不持股对应的收益
int[][] dp = new int[prices.length][2];
//初始化
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i=1;i<prices.length;i++){
//考虑都能由哪些状态得到
//要么没改变,要么卖出
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+ prices[i]);
dp[i][1] = Math.max(dp[i-1][1],-prices[i]);
}
return Math.max(dp[prices.length-1][0],dp[prices.length-1][1]);
}
122. 买卖股票的最佳时机 II
暴力搜索
贪心:
贪的方式是只要第二天比今天有增长,就考虑买入卖出。这样就可以得到总体的最大利润了。
public int maxProfit(int[] prices) {
int res = 0;
for(int i=1;i<prices.length;i++){
if((prices[i]-prices[i-1])>0){
res += prices[i]-prices[i-1];
}
}
return res;
}
动态规划:
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
// cash:持有现金
// hold:持有股票
// 状态数组
// 状态转移:cash → hold → cash → hold → cash → hold → cash
int[] cash = new int[len];
int[] hold = new int[len];
cash[0] = 0;
hold[0] = -prices[0];
for (int i = 1; i < len; i++) {
// 这两行调换顺序也是可以的
cash[i] = Math.max(cash[i - 1], hold[i - 1] + prices[i]);
hold[i] = Math.max(hold[i - 1], cash[i - 1] - prices[i]);
}
return cash[len - 1];
}
==123买卖股票的最佳时机 III ==
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
// dp[i][j] ,表示 [0, i] 区间里,状态为 j 的最大收益
// j = 0:什么都不操作
// j = 1:第 1 次买入一支股票
// j = 2:第 1 次卖出一支股票
// j = 3:第 2 次买入一支股票
// j = 4:第 2 次卖出一支股票
// 初始化
int[][] dp = new int[len][5];
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 3 状态都还没有发生,因此应该赋值为一个不可能的数
for (int i = 0; i < len; i++) {
dp[i][3] = Integer.MIN_VALUE;
}
// 状态转移只有 2 种情况:
// 情况 1:什么都不做
// 情况 2:由上一个状态转移过来
for (int i = 1; i < len; i++) {
// j = 0 的值永远是 0
dp[i][0] = 0;
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
// 最大值只发生在不持股的时候,因此来源有 3 个:j = 0 ,j = 2, j = 4
return Math.max(0, Math.max(dp[len - 1][2], dp[len - 1][4]));
}
188. 买卖股票的最佳时机 IV
377. 组合总和 Ⅳ
416. 分割等和子集
494 目标和
122. 买卖股票的最佳时机 II
位运算符:NOT,AND 和 XOR
136. 只出现一次的数字
首先,对于这种只出现一次(或者说找重复的问题)可以通过HashSet或者HashMap来解决,但有时会要求不使用额外的空间。
318. 最大单词长度乘积
67. 二进制求和
Integer.toBinaryString(Integer.parseInt(a, 2) + Integer.parseInt(b, 2));
算法的时间复杂度为 O(N+M)。但有一个问题在 Java 中,该方法受输入字符串 a 和 b 的长度限制。字符串长度太大时,不能将其转换为 Integer,Long 或者 BigInteger 类型。
33 位 1,不能转换为 Integer。65 位 1,不能转换为 Long500000001 位 1,不能转换为 BigInteger。
因此,为了适用于长度较大的字符串计算,应该使用逐比特位的计算方法。
Integer常用函数:
public String addBinary(String a, String b) {
int m = a.length(); int n = b.length();
if(n > m) return addBinary(b,a);
int carry = 0; int j = n-1;
StringBuilder sb = new StringBuilder();
for(int i = m-1; i>-1;i--){
if(a.charAt(i)=='1') ++carry;
if(j>=0 && b.charAt(j--)=='1') ++carry;
if( carry %2 ==1) sb.append('1');
else sb.append('0');
carry = carry/2;
}
//不要忘记最前面一位
if(carry ==1) sb.append('1');
//因为append是填在后面的,所以最后要翻转
sb.reverse();
return sb.toString();
}
这种解法与 2. 两数相加 的一样,都是一个进位,从后往前加;
class Solution {
//一个错的答案,没想清楚状态,状态应该是以当前位置为 结尾的 子数组的和
//所以,要返回的自然是整个dp数组的最大值
//记得填充到子数组乘积那个题
public int maxSubArray(int[] nums) {
if(nums.length == 1) return nums[0];
int n = nums.length;
int[] dp = new int[n];
//
int res= nums[0];
dp[0] = nums[0];
for(int i = 1;i<n;i++){
dp[i] = Math.max((dp[i-1] + nums[i]), nums[i]);
if(dp[i] > res) res = dp[i];
}
return res;
}
}
可以对比最大子序和,但这里的因为有负数不可以直接在 前一个 和当前 单个元素中取最大的;
自己的想法是动态规划,但是每个位置都记录两个值,一个是最大,一个是最小,然后用最大和最小的都与这个数字相乘,取大的那个
这里虽然是连续的但却用1维的,跟字符串还是不同哦。。可以考虑一下为啥不同
//子数组 连续 所以这个值要么是前一个位置传过来的,要么是重新计算的
//因为有正负,所以除了保留最大值还要保留最小值
public int maxProduct(int[] nums) {
int len = nums.length;
if(len==1) return nums[0];
int[] max = new int[len];
int[] min = new int[len];
//初始化
max[0] = nums[0];
min[0] = nums[0];
for(int i =1;i<len;i++){
max[i] = Math.max(max[i-1] * nums[i], Math.max(nums[i], min[i-1] * nums[i] ));
min[i] = Math.min(min[i-1] * nums[i], Math.min(nums[i],max[i-1]* nums[i]));
}
int res = Integer.MIN_VALUE;
for(int i =0;i<len;i++){
res = res > max[i] ? res: max[i];
}
return res;
}
//众数,排序返回中间位置的元素即可
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length/2];
}
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
暴力法:
//思路是滑动窗口,若小于k则向右扩数组,但有小数,不可
//想了DP,DFS, 滑动窗口,都不可,那就暴力吧
public int subarraySum(int[] nums, int k) {
int res = 0;
for(int start = 0;start < nums.length; start++){
int cur = 0;
for(int end = start;end < nums.length; end++){
cur += nums[end];
if(cur == k) res++;
}
}
return res;
}
前缀和法:
public int subarraySum(int[] nums, int k) {
int res = 0;
HashMap<Integer,Integer> hash = new HashMap<>();
hash.put(0,1);
int preSum = 0;
for(int i = 0;i< nums.length; i++){
preSum += nums[i];
if(hash.containsKey(preSum-k)){
res += hash.get(preSum - k);
}
hash.put(preSum, hash.getOrDefault(preSum,0)+1);
}
return res;
}
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
class Solution {
//wei le bu chongfu,jiu meici dou cong zhe ge weizhi wanghou digui
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
for(int i = 1;i<=n;i++){
LinkedList<Integer> now = new LinkedList<>();
now.add(i);
dfs(now,i, n,k);
}
return res;
}
public void dfs(LinkedList<Integer> cur, int index, int n, int k){
if(cur.size() == k){
res.add(new LinkedList(cur));
return ;
}
if(index > n) return ;
for(int i = index+1; i<= n;i++){
cur.add(i);
dfs(cur, i, n,k);
cur.removeLast();
}
}
}
public TreeNode invertTree(TreeNode root) {
if(root == null) return root;
TreeNode left = root.left;
root.left = invertTree(root.right);
root.right = invertTree(left);
return root;
}
class Solution {
//1 搜索出所有的回文字符串,然后取最长的
//2 从下往上的过程,把搜索的过程逆过来(dp)
//超时的代码
int[] res = {-1,-1,-1};
public String longestPalindrome(String s) {
if(s==null||s.length()==0) return s;
for(int i = 0;i<s.length();i++){
for(int j = i;j<s.length();j++){
if(! isParalism(s,i,j)){
continue;
}
if(j-i+1 > res[0]){
res[0] =j-i+1;
res[1] = i;
res[2] = j;
}
}
}
return s.substring(res[1],res[2]+1);
}
public boolean isParalism(String s, int l, int r){
if(l == r) return true;
while(l<=r){
if(s.charAt(l)!= s.charAt(r)){
return false;
}
}
return true;
}
}