算法学习笔记总结记录
所有的算法题第一要义就是要明白题目讲什么,明白规律是什么,只有知道了规律才能写代码,才能轮到使用什么方法,什么工具,是用递归?还是循环?进而才能用到你总结的各种经验。
下面的题目除了动态规划,其他主要来自剑指offer的题目,而且主要是数据结构题 ,排序算法我们单独整理。
一、方法技巧总结:
1、递归模板以及使用技巧总结:
-
一般递归如果如果有
if --- else
,如果你没有设置全局变量的话,一般是需要两个return的,分别在不同情况作了什么,返回各自这种情况下的结果,如果有全局变量,那么一般是没有返回值的,可以见Code015_1(com.attackOnOffer.second.Code015_01)public static void digui(ListNode listNode) { if(listNode != null) { digui(listNode.next); res.next = new ListNode(listNode.val); res = res.next; } }
-
如果有两个数组一般就不用上下边界
-
我们用到了返回boolean类型,一般样式是,xxx情况返回true,xxx情况返回fasle,最后返回xx && yy 这种样式。并且一定要有个返回true,一个要返回false;见017题
public boolean method(){ if(情况1){ return true; } if(情况2){ return false; } return method(x) & method(y); }
-
root.left = , root.right = 这种赋值操作,最后返回一个root ,参考004题
root.left = digui(); root.right = digui(); return root;
-
当你遇到有界限的时候,果断把边界放到递归参数中,而前后边界的关系就是递归的出口
public void method(int mid, int high, int low,int[] a){ int start = low; int end = high; int i = 0; int hh = mid + 1; while(i <= start && hh <= high){ //有mid的时候 // 这是归并的套路 } while(low < high){ //不需要mid的时候 // 这是快速的套路 } if(low < high){ // 这是基本套路,退出递归 } }
-
通过返回一个特殊的东西,然后添加一个if判断,如果是特殊的,就继续返回特殊的,这样能提前终止递归.参考039题。
public int method(int[] a, int count){ if(count == -1){ // 通过过特殊判断退出递归 return -1; } if(){ count == -1; return method(a,count); // 某种情况返回-1 } }
-
TreeNode node = KthNode(pRoot.left,k);不要迟疑直接先写,然后下面的 if(node != null){return node;} 记住这个模式 最后返回return null,记住这个模式!
-
返回二叉树的模板
public TreeNode digui(){ if(){ return null; } if(){ return new TreeNode(x); } root.left = digui(); root.right = digui(); return root; }
-
返回int的模板
if(){ return 0; } int left = digui() + 1; int right = digui() + 1; return Math.max(left,right); // 比较结果返回
2、思路总结:递归我们知道本质是重复做同一件事,经常的我们能够想明白这个事是怎么样的,但是却难以下手,为什么?因为如果从最开始的情况去思考,会很难理解,比如归并排序,一直二分下去,到最后只有一个元素,这怎么对比?怎么体现在代码上?容易思考得走火入魔。这个时候,我们可以直接把他当作中间的时候来写代码,这样十分容易。比如015循环算法,和归并排序(特别是归并)就是个例子。
3、说一下时间复杂度:1 < O(logn) <O(n) <O(nlogn) <O(n^2) <O(n^k) <O(2^n)
4、斐波那契数列使用的递归的复杂度是O(2^n),因为它可以看作一个颗二叉树,二叉树的高度是 n - 1,由我们的基础知识可以知道,一个高度为k的二叉树最多可以由 2^k - 1个叶子节点,也就是递归过程函数调用的次数,所以时间复杂度为 O(2^n),而空间复杂度就是树的高度 S(n)
二、链表题
链表题主要方法就是两个指针和递归和循环,我们先从这三个角度去思考。
-
003-从尾到头打印链表
办法:递归。不多提醒,你可以直接做出来。时间复杂度O(n)
public static void method(ListNode p) { if(p == null) { return ; } method(p.next); System.out.println(p.val); }
-
014-链表中倒数第k个结点
经典的两个指针,一个先动,另外一个等待k。当然java没有指针,他是new两个对象等于原链表,时间复杂度O(n)
public static ListNode method(ListNode p,int k) { ListNode p1 = p; ListNode p2 = p; int i = 0; while(p1 != null && i <= k) { p1 = p1.next; i++; } while(p1 != null && p2 != null) { p1 = p1.next; p2 = p2.next; } return p2.next; }
-
015-反转链表
三个办法,递归,利用栈,循环(这个稍微比较难写),但是复杂度都是O(n)
// 递归办法1 -- 我给出递归主要是因为我发现我不是很习惯递归返回一个值,下面这个递归是没有返回值的 // 我貌似比较喜欢全局变量,也没有错 public static ListNode res = new ListNode(0); public static ListNode ReverseList(ListNode listNode) { ListNode a = res; digui(listNode); return a.next; } public static void digui(ListNode listNode) { if(listNode != null) { digui(listNode.next); res.next = new ListNode(listNode.val); res = res.next; } } // 递归办法2 --有返回值的 public static ListNode res; public static ListNode ReverseList(ListNode listNode) { ListNode a = digui(listNode); return res.next; } public static ListNode digui(ListNode listNode) { if(listNode != null) { ListNode res = digui(listNode.next); // 这一步,你似乎不习惯递归返回值 res.next = new ListNode(listNode.val); return res.next; // 因为一定要把这个东西返回,不然没有意义 }else { ListNode temp = new ListNode(0); // 因为只会进入这里一次 res = temp; return temp; } }
循环算法,我放一张图:
这个就是中间状态,pre,next,head已经就位。但是正如我文章开头总结的一样,如果考虑最开始的pre=null思考怎么写代码,实在难以理解,无法下手。可以直接从下图的状态开始写,这个时候,我们要做的就是让head指向pre,pre移动到head的位置,next和head都移动到3的位置上。
public static ListNode method(ListNode p) {
ListNode pre = null;
ListNode head = p;
ListNode next = null;
while(head != null) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
-
016-合并两个或k个有序链表
这个你要反应过来其实就是归并排序,当然了不需要递归,O(n)
public static ListNode method(ListNode p1,ListNode p2) { ListNode res = new ListNode(0); ListNode a = res; while(p1 !=null && p2 != null) { if(p1.val >= p2.val){ res.next = new ListNode(p2.val); res = res.next; p2 = p2.next; } else { res.next = new ListNode(p1.val); res = res.next; p1 = p1.next; } } while(p1 !=null) { res.next = new ListNode(p1.val); res = res.next; p1 = p1.next; } while(p2 !=null) { res.next = new ListNode(p2.val); res = res.next; p2 = p2.next; } return a; }
-
025-复杂链表的复制
题目不明确
-
036-两个链表的第一个公共结点 — 比较重要
这个也很简单,主要是明白他的特性,我发现做题就是这样的,只要明白他的特点,代码就相对好写一点。比如这里说的的公共节点一定是从这个节点开始,他后面的一模一样,那么从后开始循环,第一个一样的就是开始节点。
主要方法:
- 两个stack,或者一个set+循环一个链表。
- 先求两个长度,然后长的先跑相差的长度,最后再一起跑
- 类似追赶问题,两个链表从头开始走,如果走到了末尾,就重新开始走,他们一定会相交于一点,可能是公共节点,可能是null;
// 和环那个有点一样是追赶问题。 // 两个链表一起跑,先跑完的在从头开始跑,这样总有一天他们会遇到相同的节点 public static ListNode method(ListNode pHead1, ListNode pHead2) { ListNode p1 = pHead1; ListNode p2 = pHead2; while(p1 != p2) { if(p1 != null) p1 = p1.next; if(p2 != null) p2 = p2. next; if(p1 != p2) { if(p1 == null) p1 = pHead1; if(p2 == null) p2 = pHead2; } } return p1; }
-
055-链表中环的入口结点---- 比较重要
这一题需要明白环是什么意思 1- 2- 3- 4- 7-3 这样就是环,同时要明白一个特点,因为它是个环,因此必定慢指针必定会追上快指针,追上之后,慢指针再从头开始走,必定会相交于环入口处
public ListNode method(ListNode pHead) { if(pHead == null || pHead.next == null || pHead.next.next == null) { return null; } ListNode slow = p.next; ListNode fast = p.next.next; while(slow != fast) { slow = slow.next; fast = fast.next.next; } slow = pHead; while(slow != fast) { slow = slow.next; fast = fast.next; } return slow; }
-
056-删除链表中重复的节点
首先他又没表达清晰,这个链表的重复节点是连续的。递归版本很厉害。简单的循环你也会。这个循环我没写出来,有点绕,要多练一下。
// 循环版本用一个record记录 public static ListNode method(ListNode pHead) { ListNode p = pHead; ListNode record = p; ListNode res = record; while(p != null){ boolean flag = false; record = p; while(p.next != null && p.next.next != null && p.next.val == p.next.next.val){ p = p.next.next; // 应该跳到next.next record.next = p; flag = true; } if(!flag) { p = p.next; record.next = p; } } return res; } // 递归版本 if (p == null || p.next == null) { // 只有0个或1个结点,则返回 return p; } if(p.val == p.next.val) { while(p != null && p.val == p.next.val) { p = p.next; } return method(p.next); } else { p.next = method(p.next); return p; }
三、二叉树题
二叉树的题几乎就是递归,因为有左右子树没法循环,只能递归
-
004-重建二叉树
Question:
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
前序遍历一定是根节点在前面,所以其实你可以认为他的每个节点都是根节点,中序遍历确定root后前面的一定是左节点,右边一定为右节点,而且中序遍历root左节点的数量==前序遍历root节点后面的数量,因为前序遍历,第一个是root,后面一定先跟着左节点。另外一个左节点树都是相同遍历方法得到,因此是个递归问题。递归总结,如果有两个数组一般就不用上下边界
public TreeNode reConstructBinaryTree(int[] pre, int[] in) { if(pre.length == 0) { return null; // 这个地方凭借思考去推觉得不对,但是直觉又告诉我肯定有个这个 } if(pre.length == 1) { return new TreeNode(pre[0]); } int rootIndex = 0; for (int i = 0; i < in.length; i++) { if (pre[0] == in[i]) { rootIndex = i; break; } } TreeNode root = new TreeNode(pre[0]); root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, rootIndex+1), Arrays.copyOfRange(in, 0, rootIndex)); root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, rootIndex+1, pre.length), Arrays.copyOfRange(in, rootIndex+1, in.length)); return root; }
-
017-树的子结构
下面是我自己的思路写的,当然还是看了答案修改了farCompare这个方法,其实这个答案是有局限性的,是不对的。我主要是想总结一下递归的特点(递归总结)。下面farCompare方法我们用到了返回boolean类型,一般样式是,xxx情况返回true,xxx情况返回fasle,最后返回xx && yy 这种样式,这个我们在文章开头总结了一下。另外就是findSomeVal这个我们写的很垃圾,一般不会有a = findSomeVal(root.left, target); a = findSomeVal(root.rigth, target);这种同时存在,因为这样完全不知道要返回什么。参考上一题,通常样式是root.left = , root.right = 这种赋值操作,最后返回一个root,这个我们也在文章开头中总结了模板。
// 正宗解法 public boolean HasSubtree(TreeNode root1,TreeNode root2) { boolean result = false; if(root2 == null || root1 == null) { return false; } if(root1.val == root2.val){ result = subMethod(root1,root2); } if(!result) result = HasSubtree(root1.left,root2); if(!result) result = HasSubtree(root1.right,root2); return result; } public boolean subMethod(TreeNode root1,TreeNode root2){ if(root1 == null && root2 != null){ return false; } if(root1 == null && root2 == null){ return true; } if(root2 == null){ return true; } if(root1.val == root2.val){ return subMethod(root1.left,root2.left) && subMethod(root1.right,root2.right) } return false; }
public static TreeNode some; public static boolean HasSubtre(TreeNode root1,TreeNode root2) { TreeNode a = findSomeVal(root1,root2.val); if(a != null) { return farCompare(a, root2); } return false; } public static TreeNode findSomeVal(TreeNode root, int target) { TreeNode a; if(root == null) { return null; } if(root.val == target) { return root; } a = findSomeVal(root.left, target); if(a != null) { return a; } a = findSomeVal(root.right, target); if(a != null) { return a; } return null; // if(root != null) { // 指的是这里! 一般是root.left = /root.right = 最后返回root // if(root.val != target) { // findSomeVal(root.left, target); // findSomeVal(root.right, target); // } else { // some = root; // } // } } public static boolean farCompare(TreeNode root1, TreeNode root2) { if(root2 == null) { return true; } if(root1 == null && root2 != null) { return false; } if(root1.val != root2.val) { return false; } return farCompare(root1.left, root2.left) && farCompare(root1.right, root2.right); // if(root1 != null && root2 != null && root1.val == root2.val) { // farCompare(root1.left, root2.left); // farCompare(root1.right, root2.right); // return true; // 我一直在纠结怎么返回true,你看看上面那个返回 if(root2 == null) {return true;},我似乎有点感觉了 // } else { // return false; // } }
-
018-二叉树的镜像
这个就是递归,而且很简单
public static boolean method(TreeNode root) { TreeNode r1 = root; TreeNode r2 = root; return subMethod(r1,r2); } public static boolean subMethod(TreeNode root1, TreeNode root2) { if(root1 == null && root2 != null) { return false; } if(root1 != null && root2 == null) { return false; } if(root1 == null && root2 == null) { return true; } if(root1.val != root2.val) { return false; } return subMethod(root1.left, root2.right) && subMethod(root1.right, root2.left); }
-
022-从上往下打印二叉树
这个也很简单,借助队列来实现,或者使用list.remove(0)这个也行,不用递归,循环就可以了
-
023-二叉搜索树的后序遍历序列
确认一个数组是否是后续遍历的结果,规律是最后一个节点一定是根节点,然后前面比他小的一定是他的左节点,比他大的一定是他的右节点。因此如果右边有比他小的那么就一定是不对的。
这里我想递归总结一点,就是当你遇到有界限的时候,果断把边界放到递归参数中,而前后边界的关系就是递归的出口。而往往最后一个return就是xx && yy,对比二叉树第一题,他因为有两个数组,不好使用前后边界参数。
public static boolean VerifySquenceOfBST(int [] sequence,int start,int end) { if(start >= end) { return true; } int root = sequence[end]; int i = 0; while(sequence[i] < root) { i++; } int j = i; while(j > root) { if(sequence[j] < root) { return false; } j++; } return VerifySquenceOfBST(sequence,start,j-1) && VerifySquenceOfBST(sequence, j, end-1); }
-
024-二叉树中和为某一值的路径
首先这个路径代表从头到尾,而不是中间不完全的路径。其次我们以后遇到有求和的,我们要巧用target-xx这种样式,因为这样我们就只要比较当前值 和 差值是否相等就可以了。很巧妙。
static ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>(); public static ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) { ArrayList<Integer> temp = new ArrayList<Integer>(); subMethod(temp,root,target); return res; } private static void subMethod(ArrayList<Integer> temp, TreeNode root, int target) { temp.add(root.val); // if的条件很重要,尤其是后面那两个 == null if(root.val == target && root.left == null && root.right == null) { res.add(new ArrayList <Integer>(temp)); // return; 这里不用return } if(root.left != null) { subMethod(temp,root.left,target - root.val); } if(root.right != null) { subMethod(temp,root.right,target - root.val); } temp.remove(temp.size()-1); }
-
026-二叉搜索树与双向链表
看不懂
-
038-二叉树的深度
这题比较比较不好理解,最小的情况是这样,如果有一个二叉树,只有左节点,没有右节点,那么我们先遍历左节点得到1,右节点得到0,使用本质就是比较一个节点左边有还是右边有节点,返回最大的那个值。因为这个方法不用开辟空间,因此要学会。这个递归太抽象了。
public int TreeDepth(TreeNode root) { if(root == null) { return 0; } int left = TreeDepth(root.left) + 1; int right = TreeDepth(root.right) + 1; return left >= right ? left : right; // 相当于一个root节点下是否有左右节点,看谁比较大,谁大那么谁就是深度 }
-
039-平衡二叉树
看我们改进的代码,递归总结:通过返回一个特殊的东西,然后添加一个if判断,如果是特殊的,就继续返回特殊的,这样能提前终止递归;
public int countFor(TreeNode root, int count){ if(root == null){ return -1; } if(count ) count++; int left = countFor(root.left,count); int right = countFor(root.right,count); if(Math.abs(left - right) > 1){ flag = false; } return Math.max(left,right); } // 看了其他人的代码 改进的: if(root == null) { return count; // 确实是,如果最后一个没有的话应该返回之前计算的长度 } if(count == -1){ // 判断是否是那个特殊的字符,这样能提前结束递归 return -1; } count++; int left = judgeIsBalance(root.left,count ); int right = judgeIsBalance(root.right, count); if(Math.abs(left-right)>1) { return -1; // 返回一个特殊的字符 } return Math.max(left, right);
-
057-二叉树的下一个结点
无语的题目,表述不清,没意思;
public TreeLinkNode GetNext(TreeLinkNode node) { if(node==null) return null; if(node.right!=null){ //如果有右子树,则找右子树的最左节点 node = node.right; while(node.left!=null) node = node.left; return node; } while(node.next!=null){ //没右子树,则找第一个当前节点是父节点左孩子的节点 if(node.next.left==node) return node.next; node = node.next; } return null; //退到了根节点仍没找到,则返回null }
-
058-对称的二叉树
经典的递归样式,不管你传left还是right 我就是比较左val 是否 等于 右val
public boolean mirro(TreeNode root1,TreeNode root2){ if(root1 == null && root2 != null){ return false; } if(root2 == null && root1 != null) { return false; } if(root2 == null && root1 == null){ return true; } if(root2.val != root2.val){ return false; } return mirro(root1.left,root2.right) && mirro(root2.left,root1.right); }
-
059-按之字形顺序打印二叉树
这个题很简单了,就是两个stack,奇数行怎么存进去,偶数行怎么存进去。
-
062-二叉搜索树的第k个结点
这个我们能反映过来是中序遍历。递归总结:TreeNode node = KthNode(pRoot.left,k);不要迟疑直接先写,然后下面的 if(node != null){return node;} 记住这个模式 最后返回return null,记住这个模式!
int index = 0; TreeNode KthNode(TreeNode pRoot, int k) { if(pRoot == null){ return null; } TreeNode node = KthNode(pRoot.left,k); if(node != null){ return node; } index ++; if(index == k){ return pRoot; } node = KthNode(pRoot.right,k); if(node != null){ return node; } return null; }
-
064-滑动窗口的最大值(双端队列)
其实本质是构造一个单调递减的队列
/** * f滑动窗口 * r始终保持队头元素最大,然后后面的元素单调递减 * r因此就有,如果新来的元素比队尾大,就移除队尾的元素---这样就会导致可能比队头的还要大,把队头也给移除了,是正确的 * r如果新来的比对尾小,那就就放入队尾 * r判断队头的元素是否不应该在队列中,不实的话就移除 * r例如 2,3,4,2,6,2,5,1 * 2 * 3 * 4 * 4 2 * 6 * 6 2 * 6 5 * 5 1 --- 这里就是计算出6是不属于队列的 * @author fongfiafia */ public class Code064 { public ArrayList<Integer> maxInWindows(int[] num, int size) { ArrayList<Integer> res = new ArrayList<>(); if (size == 0) return res; Deque<Integer> q = new LinkedList<>(); for (int i = 0; i < num.length; i++) { while (!q.isEmpty() && num[q.getLast()] < num[i]) { q.pollLast(); } q.addLast(i); // 这里如果进入了上面的循环 addlast 就等于addfirst while (!q.isEmpty() && q.getFirst() <= i - size) { q.pollFirst(); } if (i >= size - 1) { res.add(num[q.getFirst()]); } } return res; } }
-
034-第一个只出现一次的字符
-
public char method(String str){ int[] temp = int[56]; for(int i = 0; i < str.length; i++){ temp[(int)str.charAt(i)]+=1; } for(int i = 0; i < str.length; i++){ if(temp[(int)str.charAt(i)] - 56 == 1){ return str.charAt(i); } } }
四、动态规划
为了快速学习,我参考了动态规划 这位的动态规划思路,这样上手快一点。
动态规划的核心思想是:数学归纳法。有点忘记了什么是数序归纳法,举个例子你就知道了:
证明:S(n) = 1 + 2 + 3 …. + n 前n项和为n(n + 1) / 2
n = 1, S(1) = 1
假设n时命题成立 ----------假设某个时候成立,证明他后面的是否也成立
N+ 1时,
S(n + 1)
= S(n) + n + 1
= n(n + 1)/2 + n + 1
= (n + 1)(n + 2)/ 2
因此成立
- 问题一:
为什么会想到他是动态规划呢?emm,我还没搞明白,就目前我们掌握的,递归,循环,二分似乎都用不上,那么就只剩下动态规划?暂时先这么想吧。
根据那篇文章我记录一下体会:
第一步:动态规划一定有一个数组dp[],我们首先就要想好这个数组表达什么意思,其实很明显,这个数组应该是:dp[5] 表示num[5] 他的最长上升序列长度是多少?因此我们的最终返回结果应该是:
for (int i = 0; i < dp.length; i++) {
res = Math.max(res, dp[i]);
}
第二步:dp[5]这个值怎么算的呢?这当然才是关键,而你的思考应该是dp[5]怎么通过dp[0]----dp[4]的来的到dp[5]。很明显的其实应该是index = 5这个前面第一个小于他的index 对应的dp[index] + 1 就是 dp[5] 的值。当然了这里他用的从头开始比较:–我要试验一下我的方法,当然不影响复杂度其实。但是他这个思想我认为是最朴素的,最暴力的,我觉得一开始可以按他这么来想。其次这个模式: dp[i] = Math.max(dp[i], dp[j] + 1); 也是常用模式。
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
更进一步的,所有dp[n]都可以写出来:
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
第三步:细节dp[n]应该要初始化大小为1,这个初始化也是动态规划必须的动作。
验证一下我的想法,也就是从n-1开始比较,不要再从头比较了:这样是正确的,当然其实复杂度不会变。
for(int j = n-1; j >= 0; j--) {
if(num[n] >= num[j]) {
dp[n] = dp[j] + 1;
break;
}
}
-
问题二:凑硬币:给你
k
种面值的硬币,面值分别为c1, c2 ... ck
,每种硬币的数量无限,再给一个总金额amount
,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。算法的函数签名如下:解答:根据上面的三步走:
1、确定dp[],以及他的含义。这里的话我们第一次想错了,其实也不应该错,根据【状态】这个东西,我们知道不会变得应该是amount,目标值,他应该就是状态,也就是dp[]应该是:目标金额为n时,需要dp[n]个硬币。对比上面子序列,上面是:当num下标为n时,他的最长字串的dp[n],可以看出你的最后结果都是dp[],只不过,dp的下标代表不同含义。
2、分解子问题,已知dp[0-----n-1],怎么得到dp[n]呢?,思考得到,如果当前硬币面额是3元,我要求dp[10],那么就一定是dp[7] + 1,所以需要遍历coins数组
for(int coin : coins) { if((n - coin) < 0) { continue; } dp[n] = Math.min(dp[n], dp[n-coin] + 1); // 我要求8元需要的硬币数量,并且还有3元硬币可以选择,那么一定是5元需要的数量 + 1 }
3、给基本情况+初始化。dp[0] = 0,这里总结,如果求最大值,给dp初始化0,如果求最小值,给dp初始化最大值。
public static int coinChange(int[] coins, int amount) { // 我们定义dp是对应那个下标的coins 的数量 -- 这样定义不对,这样定义我们没法进行下一步 // 应该定义为当前的目标金额是 n,至少需要 dp(n) 个硬币凑出该金额。 int[] dp = new int[amount+1]; Arrays.fill(dp, amount+1); // 这个初始化有讲究的,相当于是给个最大值(可以记住是 求最小 值是初始化正无穷,求最大值时候初始化0) dp[0] = 0; // 给个基本情况 for(int n = 0; n < amount+1 ; n++) { for(int coin : coins) { if((n - coin) < 0) { continue; } dp[n] = Math.min(dp[n], dp[n-coin] + 1); } } return dp[amount] == amount + 1? -1 : dp[amount]; }
-
问题三:给你一个可装载重量为
W
的背包和N
个物品,每个物品有重量和价值两个属性。其中第i
个物品的重量为wt[i]
,价值为val[i]
,现在让你用这个背包装物品,最多能装的价值是多少?也是三步走
1、构建数组,这里明显有两个状态因此就是大胆的给他定为二维数组:
dp[][]
;但是关键还是要定义出这个二维数组是表示什么含义,应该是dp[i][w] = val
表示:前i个物品,在背包容量为w的时候,最多放价值val。总结:我们可以看到dp数组的值一定是我们最后要的东西,比如最长序列是序列长度,硬币问题是硬币数量。其次就有两个遍历for。2、分解子问题,已知xx,求
dp[i][w]
。思考的方向其实和硬币的一样,就是w-遍历物品的重量,然后就是前一个val+减去遍历物品的val,表达式是:dp[i][w] = dp[i-1][w-weight[i-1]] + val(i)
。然后他有个选择问题,就是要不要把第i个物品放进去,如果不放那么就是dp[i-1][w]
。综合到代码就是:// w是要求小于的重量 n是几个物品 public static int method(int w, int n, int[] wt,int[] val) { // 对于前i个物品,放在容量为w的背包中,价值dp[i][w] int[][] dp = new int[n+1][w+1]; for(int i = 1; i < n+1; i ++) { for(int j = 1; j < w+1; j++) { if(j - wt[i-1] < 0) { dp[i][j] = dp[i-1][j]; // 为什么是j-1,只是因为j是从1开始的,j-1就是j } else { dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j - wt[i-1]] + val[i-1]); // 这个j - wt[i-1]和凑硬币是一样的 } } } return dp[n][w]; }
能装的价值是多少?
也是三步走
1、构建数组,这里明显有两个状态因此就是大胆的给他定为二维数组:dp[][]
;但是关键还是要定义出这个二维数组是表示什么含义,应该是dp[i][w] = val
表示:前i个物品,在背包容量为w的时候,最多放价值val。总结:我们可以看到dp数组的值一定是我们最后要的东西,比如最长序列是序列长度,硬币问题是硬币数量。其次就有两个遍历for。
2、分解子问题,已知xx,求dp[i][w]
。思考的方向其实和硬币的一样,就是w-遍历物品的重量,然后就是前一个val+减去遍历物品的val,表达式是:dp[i][w] = dp[i-1][w-weight[i-1]] + val(i)
。然后他有个选择问题,就是要不要把第i个物品放进去,如果不放那么就是dp[i-1][w]
。综合到代码就是:
// w是要求小于的重量 n是几个物品
public static int method(int w, int n, int[] wt,int[] val) {
// 对于前i个物品,放在容量为w的背包中,价值dp[i][w]
int[][] dp = new int[n+1][w+1];
for(int i = 1; i < n+1; i ++) {
for(int j = 1; j < w+1; j++) {
if(j - wt[i-1] < 0) {
dp[i][j] = dp[i-1][j]; // 为什么是j-1,只是因为j是从1开始的,j-1就是j
} else {
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j - wt[i-1]] + val[i-1]); // 这个j - wt[i-1]和凑硬币是一样的
}
}
}
return dp[n][w];
}
3、数据初始化,其实应该是第一行和第一列为0,但是数组已经帮忙我们全部定义为0了