Catalogue
- 周刷题总结
- 每日一题
- 22.7.16 [无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters)(滑动窗口)(HashMap类)
- 22.7.19 [最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/)(dp、中心扩散法)
- 22.7.20 [盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/)(双指针)
- 22.7.23 [环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/)(双指针)
- 22.7.24 [每日温度](https://leetcode.cn/problems/daily-temperatures/)(单调栈)
- 22.7.25 [接雨水](https://leetcode.cn/problems/trapping-rain-water/)(DP、单调栈、双指针)
- 22.7.26 [柱状图中最大的矩形](https://leetcode.cn/problems/largest-rectangle-in-histogram/)(单调栈)
- 22.8.3 [合并K个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/)(归并分而治之、优先级队列)(PriorityQueue类、比较器)
- 22.8.11(回溯合集,组合问题:N个数里面按一定规则找出k个数的集合)
- 22.8.31(dp合集,0-1背包问题:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。)
- Pytorch知识
周刷题总结
一、7.31-8.6(回溯、比较器)
easy
- 移除元素:原地移除数组中值为val的元素,o(1)的空间复杂度要求。
双指针思想: fast 和 slow 两个指针控制,快指针 fast 指向当前要和 val 对比的元素,慢指针 slow 指向将被赋值的位置。
- 数组中重复的数字:长为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。找出该数组中任意一个重复数字。
- 构建乘积数组:给定一个数组A[0,1,…,n-1],构建一个数组B[0,1,…,n-1] ,其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1] 。不能使用除法。
字典:利用条件在0-n-1范围内,建立一个索引的值的数组,一但a[i]出现,idx[a[i]]++,判断是否有重复;该题有一个原地交换的解法,后续要了解一下(考虑空间)
定义两个数组:left, right,两次循环求出i位置左边的累乘、右边的累乘。
- 环形链表 II:找到环形链表的入环节点。
- 除数博弈:黑板上有一个数字 n 。在每个玩家的回合,玩家需要执行以下操作:
-
- 选出任一 x,满足 0 < x < n 且 n % x == 0 。
-
- 用 n - x 替换黑板上的数字 n 。
- 如果玩家无法执行这些操作,就会输掉游戏。
- 翻转卡片游戏:n张卡片,正面和背面都写着一个正数, 可以任意翻转, 输出 满足背面的数字与任意一张正面的数字都不同 的这些数中的最小值。
- 删除注释:删除// /* */之间的注释,当成“”。
- 合并两个有序链表:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
双指针:快慢指针,得到第一次的相交点,然后让快指针回到原点,慢指针在交点,同速度.next,交点即为入环点。
数学题:假如n为奇数,则它的因数都为奇数,N-x之后 那就变成偶数了,Bob给他-1,那么alice永远拿奇数,最后Bob一定拿到最小偶数2;如果n为偶数,Alice只要将其变成奇数就必赢了
阅读理解:如果某个位置正反面数字一样,那么相同的数字在其他位置是肯定行不通的。所以记录出现相同情况的位置,只要后续的最小值不属于出现相同情况的位置就可以。
分情况讨论:只能通过for循环每一行 逐个单词来判断。因为会出现/* */ // 在一行的情况。
迭代:设置一个哨兵节点,可以避免判空;递归对于本题感觉更优。
medium
- 不同路径 III:1代表起始 2代表结束 0代表可以通过 -1代表无法越过的障碍。返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。 每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格。
回溯:四个方向回溯,判断是否超出边界、是否为曾经走过的点、碰到障碍、碰到起点。
- 把数组排成最小的数:输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
比较器:工具是用自定义排序,来排序数组中元素的位置;思路是比较x+y 与 y+x
二、8.8-8.12(二叉树、大根堆、比较器)
队列的工具: Queue queue = new LinkedList<>()
堆的工具:Queue queue = new PriorityQueue<>() 默认小根堆
easy
- 序列化二叉树:请实现两个函数,分别用来序列化和反序列化二叉树。也就是将二叉树变成String ,将String变成二叉树。
BFS按层遍历思想: 利用队列结构(LinkedList),先存一个非空树节点,遍历直到空:出队列,判断左右子树,非空进队列。
// 注意反序列化时 还是需要用队列存储
Queue<TreeNode> treeNodes = new LinkedList<>();
treeNodes.offer(root);
while (!treeNodes.isEmpty()){
root = treeNodes.poll();
if (root != null){
res.append(root.val).append(",");
treeNodes.offer(root.left);
treeNodes.offer(root.right);
} else
res.append("null,");
}
- 螺旋矩阵 II:给你一个正整数 n ,生成一个包含 1 到 n 2 n^2 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
模拟思想: 定义四个角来模拟 好理解。
int now = 1;
int left = 0, right = n-1, up = 0, down = n-1;
while (now <= n*n){
for (int j = left; j <= right; j++) // 从左到右 0->n-1
res[up][j] = now++;
up++; // 代表上面一层已经结束 ++
for (int i = up; i <= down; i++) // 从上到下 1->n-1
res[i][right] = now++;
right--; // 代表最右边一层已经结束++
for (int j = right; j >= left; j--) // 从右到左 n-2 ->0
res[down][j] = now++;
down--; // 代表最下面一层已经结束 ++
for (int i=down; i >= up; i--) // 从下到上 n-2 -> 1
res[i][left] = now++;
left++; // 代表左边一层已经结束了 ++
}
medium
- 任意子数组和的绝对值的最大值:求整数数组子数组和的绝对值的最大值
dp思想:dp[i]代表i位置结尾的子数组最大值。
dp[i] = dp[i-1]+num[i], dp[i-1]>0 | num[i], dp[i-1]<=0
。如果前一位置和小于0了那么越加越少,只取现在,否则与前一位置合并。
int dp_max = 0, dp_min = 0;
for (int num : nums) {
dp_max = dp_max > 0 ? dp_max+num : num;
dp_min = dp_min < 0 ? dp_min+num : num;
ans = Math.max(ans, Math.max(dp_max, -dp_min));
}
前缀和思想: 这个数组前缀和的最大值和最小值,由最小值变成最大值的差值肯定就是子数组的最大值。 这里将最小值初始化为0,这样如果求和过程没有<0的,根本不会更新,保证求差不会减少(此时的最大值就是ans)
int sum = 0, f_max = 0, f_min = 0;
for (int num : nums) {
sum += num;
f_max = Math.max(f_max, sum);
f_min = Math.min(f_min, sum);
}
- 下降路径最小和 II:给你一个n x n 整数矩阵 grid,请你返回 非零偏移下降路径 数字和的最小值。
dp思想: dp[i][j] = min(dp[i-1][k]) + num[i][j], k!=j 找出不在同一列的最小值
for (int i = 0; i < n; i++)
dp[0][i] = grid[0][i];
for (int i = 1; i < n; i++)
for (int j = 0; j < n; j++){
dp[i][j] = Integer.MAX_VALUE;
for (int k = 0; k < n; k++)
if (k != j)
dp[i][j] = Math.min(dp[i][j], grid[i][j]+dp[i-1][k]);
}
这个题有很大的优化空间,目前时间o( n 3 n^3 n3),空间o( n 2 n^2 n2),可以通过只记录上一阶段的最小值、最小值索引、次小值降低至时间o( n 2 n^2 n2)空间o( 1 1 1)。因为其实只与上一阶段的最小值有关系,如果索引和和最小值在一列,那肯定就是次小值了。
- 合并 K 个升序链表:给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合并后的链表。
大根堆、比较器思想: 将每个list放入大根堆中,谁大谁先出来,然后放入他的next(非空)
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
// 将每个list放入小根堆中,谁小谁先出来,然后放入他的next(非空)
ListNode res = new ListNode(-1);
ListNode head = res;
Queue<ListNode> queue = new PriorityQueue<>(new PriorityComparator());
for (ListNode list : lists) {
if (list != null)
queue.offer(list);
}
while (!queue.isEmpty()) {
ListNode node = queue.poll();
head.next = node;
head = head.next;
if (node.next != null)
queue.offer(node.next);
}
return res.next;
}
}
class PriorityComparator implements Comparator<ListNode> {
@Override
public int compare(ListNode o1, ListNode o2) {
return o1.val - o2.val; // 升序排列
}
}
递归思想: 多次递归两个链表的升序合并。
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}
- 剪绳子:将一个长度为 n的绳子裁剪为整数长度 m段 m、n>1,求裁剪的 m段绳子乘积的最长值
dp思想:dp方法 dp[i]代表所能裁剪的最大值,0、1都不能裁剪所以为0。 i裁剪出来 a和b, 分为两种情况a、b均不继续裁剪;a不裁剪,b继续裁剪。
所以转移方程 max( a*(i-a), a*dp[i-a] )
int[] dp = new int[n + 1];
for (int i = 2; i <= n; i++)
for (int a = 1; a < i; a++)
dp[i] = Math.max(dp[i], Math.max(a*(i-a), a*dp[i-a]));
数论思想:数学前提:每个大于4的数一定可以拆分成两个数使乘积大于4。所以限制了拆数组合一定是2和3。由于3^2 > 2^3 所以多拆3肯定会得到max
if (n <= 3)
return n-1;
int num3 = n / 3;
n = n % 3;
if (n == 0)
return (int)Math.pow(3, num3);
else if (n == 1) { // 这里注意!!1 代表余的是4,应该分成2 2 而不是1 3
return (int)(Math.pow(3, num3-1) * 4);
}else {
return (int)Math.pow(3, num3) * 2;
}
三、8.14-8.16(二叉树、滑动窗口)
easy
- 礼物的最大价值:在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。计算最多能拿到多少价值的礼物?
dp思想: 可以直接一维数组,初始化等于第一层的数据 dp[i] 就代表此时该行 i列 拿礼物的最大值。```dp[i] = max(dp[i-1],dp[i])````前一个i-1是左边那一列,后一个i是上一行i列
public int maxValue(int[][] grid) {
int[] dp = new int[grid[0].length];
for (int[] line : grid) { // 第一行初始化也可以在这里进行不影响
dp[0] += line[0]; // 第一列肯定是累加的
for (int i = 1; i < line.length; i++){
dp[i] = Math.max(dp[i-1], dp[i]) + line[i];
}
}
return dp[grid[0].length-1];
}
- 合并二叉树:两颗二叉树合并,位置覆盖,重叠的节点两个值相加
递归思想: 最简单
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1 == null)
return root2;
if (root2 == null)
return root1;
root1.val += root2.val;
root1.left = mergeTrees(root1.left, root2.left);
root1.right = mergeTrees(root1.right, root2.right);
return root1;
}
BFS按层遍历思想:迭代你要搞清楚,只要二叉树的左右节点一个为null,那么res的下一个节点肯定直接等于非null了。 首先要创建三个LinkedList对象,
public TreeNode mergeTrees1(TreeNode root1, TreeNode root2) {
if (root1 == null)
return root2;
if (root2 == null)
return root1;
Queue<TreeNode> queue1 = new LinkedList<>();
queue1.offer(root1);
Queue<TreeNode> queue2 = new LinkedList<>();
queue2.offer(root2);
TreeNode res = new TreeNode(root1.val + root2.val);
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(res);
while (!queue1.isEmpty() && !queue2.isEmpty()) { // 有一个为空了,代表都等于另外一个了
TreeNode node1 = queue1.poll(), node2 = queue2.poll(), node = queue.poll();
TreeNode left1 = node1.left, left2 = node2.left, right1 = node1.right, right2 = node2.right;
if (left1 != null || left2 != null)
if (left1 != null && left2 != null) { // 两个都不为空 ,要继续合并
node.left = new TreeNode(left1.val + left2.val);
queue1.offer(left1);
queue2.offer(left2);
queue.offer(node.left);
} else if (left1 != null)
node.left = left1; // 左端直接拦住 不需要在遍历了
else
node.left = left2;
if (right1 != null || right2 != null)
if (right1 != null && right2 != null) {
node.right = new TreeNode(right1.val + right2.val);
queue1.offer(right1);
queue2.offer(right2);
queue.offer(node.right);
} else if (right1 != null) {
node.right = right1;
} else
node.right = right2;
}
return res;
}
medium
- 长度最小的子数组:给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0 。
滑动窗口思想: 一旦sum达到指定数值,此时肯定是右边满足,所以左移来判断,能否减少长度。 此时 只需要
sum>=target
一个来判断就可,因为left一旦大于right肯定是先为0(全为正),而target>=1 ,所以不用担心left超过right
public int minSubArrayLen(int target, int[] nums) {
int res = Integer.MAX_VALUE;
int left = 0, right = 0, sum = 0;
while (right < nums.length){
sum += nums[right];
while (sum >= target && left<=right){
// 肯定是右边突然来了大的了
res = Math.min(res, right-left+1);
sum -= nums[left];
left++;
}
right++;
}
if (res == Integer.MAX_VALUE)
return 0;
else
return res;
}
四、8.22-8.25(二叉树、滑动窗口)
easy
- 统计二叉树中好节点的数目:请你返回二叉树中好节点的数目。 好节点」X 定义为:从根到该节点 X 所经过的节点中,没有任何节点的值大于 X 的值。
递归思想: 在递归的过程中,带着从根节点到当前节点(不包括)的所有节点的最大值max。 本题递归是最好的解法。
public int goodNodes(TreeNode root) {
return f(root, root.val);
}
public int f(TreeNode root, int max){ // 在递归的过程中,带着从根节点到当前节点(不包括)的所有节点的最大值max,
if (root == null)
return 0;
int max_num = 0;
if (root.val >= max){
max_num++;
max = root.val;
}
return f(root.left, max) + f(root.right, max) + max_num;
}
- 最小的k个数:输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
大根堆排序思想: 建立大根堆,得到一个最大值,这样谁比这个最大值大,肯定就不能进去,不为最小k个数。 可以很好的练习排序。
public int[] getLeastNumbers(int[] arr, int k) {
if (k == 0)
return new int[0];
// 这里建立一个大小为K的大根堆 因为只要比堆顶元素大 那就进队
Queue<Integer> heap = new PriorityQueue<>((v1, v2) -> v2-v1); // 大根堆 升序
for (int i : arr) {
if (heap.size() < k)
heap.offer(i);
else if (heap.peek() > i) {
heap.poll();
heap.offer(i);
}
}
int[] ans = new int[k];
int idx = 0;
for (Integer integer : heap) {
ans[idx] = integer;
idx++;
}
return ans;
}
medium
- 移动片段得到字符串:给你两个字符串 start 和 target ,长度均为 n 。每个字符串 仅 由字符 ‘L’、‘R’ 和 ‘_’ 组成,通过移动判断start能否转化为target。
逻辑题双指针思想:三条标准:去掉"_"之后start==target ; start的“L”在target的右边; start的“R“在target的左边。
public boolean canChange(String start, String target) {
int i = 0, j = 0, n = start.length(); // i、j两个双指针指向start、target的当前位置
while (i < n && j < n){
while (i < n && start.charAt(i) == '_') // 当碰到_就没用 继续index++
i++;
while (j < n && target.charAt(j) == '_')
j++;
if (i >= n || j >= n)
break; // 防止超出界 "L____"这种情况
if (start.charAt(i) != target.charAt(j))
return false; // 去掉_之后start和target的值应该一致
if (start.charAt(i) == 'L'){
if (i < j)
return false; // start的L在target的左边 不可以
}
if (start.charAt(i) == 'R'){
if (i > j)
return false; // start的R在target的右边 不可以
}
i++;
j++;
}
while (i < n){
if (start.charAt(i) != '_')
return false;
i++;
}
while (j < n){
if (target.charAt(j) != '_')
return false;
j++;
}
return true;
}
五、8.27-9.1(区间问题、BFS、图)(建议后续看看)
easy
- 买钢笔和铅笔的方案数:给你一个整数 total ,表示你拥有的总钱数。同时给你两个整数 cost1 和 cost2 ,分别表示一支钢笔和一支铅笔的价格。你可以花费你部分或者全部的钱,去买任意数目的两种笔。请你返回购买钢笔和铅笔的 不同方案数目 。
error:两层循环 分别表示钢笔、铅笔的购买数目
数学枚举思想:枚举出钢笔的数目x 那么铅笔的数目就是(total - x*cost1) / cost2。第二层不要累积一个一个算,要使用数学知识。
public long waysToBuyPensPencils(int total, int cost1, int cost2) {
// 枚举出钢笔的数目x 那么铅笔的数目就是(total - x*cost1) / cost2
long res = 0;
for (int x = 0; x * cost1 <= total; x++){
int y = (total - x*cost1) / cost2 + 1; // 0支铅笔没有计算在内 所以+1
res += y;
}
return res;
}
medium
- 合并区间:以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [start_i, end_i] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
排序思想: 对区间进行排序,谁的start小 谁就在前面;start相同的end小的在前面。这种情况下:前一个end >= 后一个start 势必包含,[前一个start,max(end前,end后)】
class MergeComparator implements Comparator<int[]> {
@Override
public int compare(int[] a1, int[] a2) {
// 按照start的升序排列
if (a1[0] != a2[0])
return a1[0] - a2[0];
else
return a1[1] - a2[1];
}
}
public int[][] merge(int[][] intervals) {
List<int[]> list = new ArrayList<>();
Arrays.sort(intervals, new MergeComparator());
for (int[] interval : intervals) {// 已经排好序了
int l = interval[0], r = interval[1], n = list.size();
if (n == 0 || list.get(n-1)[1] < l) // 这种情况就是添加新的数组
list.add(new int[]{l, r});
else // 修改右边的边界
list.set(n-1, new int[]{list.get(n-1)[0], Math.max(list.get(n-1)[1], r)});
}
return list.toArray(new int[list.size()][]); // 调用了copyOf函数
}
- 插入区间:向一个按照区间起点排序好的区间列表中插入一个新的区间。需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。
模拟思想: 其实就是有交集 无交集:有交集就是合并区间,无交集 就是添加了;如果左侧无交集,那就是直接添加inter;右侧无交集,那要判断new是否添加了,然后添加inter
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> list = new ArrayList<>();
boolean append = false; // 用来标记new是否添加了
for (int[] interval : intervals){
if (interval[1] < newInterval[0]) // 在new的左侧 无重叠
list.add(interval);
else if (interval[0] > newInterval[1]) { // 在new的右侧 无重叠
if (!append){
append = true;
list.add(newInterval);
}
list.add(interval);
}
else { // 代表有交集,更新左右
newInterval[0] = Math.min(newInterval[0], interval[0]);
newInterval[1] = Math.max(newInterval[1], interval[1]);
}
}
if (!append) list.add(newInterval);
return list.toArray(new int[list.size()][]);
}
- 带因子的二叉树:给出一个含有不重复整数元素的数组 arr ,每个整数 arr[i] 均大于 1。 1 <= arr.length <= 1000。用这些整数来构建二叉树,每个整数可以使用任意次数。其中:每个非叶结点的值应等于它的两个子结点的值的乘积。 求满足条件的二叉树个数?
记忆化搜索DP思想: 根据 不包含1->根节点往下的路径都是递减的 -> 升序排序
本质是两数之积–>在下标为[0,i-1)中,枚举 == arr[i]的 arr[left]*arr[right] 0<=left<=right<i
dp[i]代表i位置能组成二叉树的数目(初始化dp[i]=1) dp[i]=dp[l]*dp[r]*2, left!=right || dp[i]=dp[l]*dp[r, left==right
public int numFactoredBinaryTrees(int[] arr) {
Arrays.sort(arr);
long res = 0, mod = (long)1e9+7;
int n = arr.length;
long[] dp = new long[n];
for (int i = 0; i < n; i++){
dp[i] = 1;
for (int left = 0, right = i-1; left<=right; left++){ // 最外层的大循环,决定left++的
while (left<=right && (long)arr[left]*arr[right] > arr[i])
right--; // 左右两端点乘积比i大时,减少乘积 r左移
if (left <= right && (long)arr[left]*arr[right] == arr[i]){ // int的范围是20亿 注意范围
if (left == right)
dp[i] += dp[left] * dp[right];
else
dp[i] += dp[left] * dp[right] * 2;
}
}
res = (res + dp[i]) % mod;
}
return (int)res;
}
- 到家的最少跳跃次数:有一只跳蚤的家在数轴上的位置 x 处。请你帮助它从位置 0 出发,到达它的家。它可以 往前 跳恰好 a个位置(即往右跳)。它可以 往后跳恰好 b个位置(即往左跳)。它不能 连续 往后跳 2 次。它不能跳到任何forbidden数组中的位置。跳蚤可以往前跳 超过 它的家的位置,但是它 不能跳到负整数 的位置。
BFS 搜索最短路径思想: BFS 的数据范围 当a<b时(a1999、b2000、forbidden1998都接近2000),一开始肯定是a先动,一旦x位于2000
那么a要动两步->4000了 但是减去b就是forbidden了,所以a动三个 也就是右边不会超过6000
初始两个数组 分别记录禁止位置、已经访问过的位置
public int minimumJumps(int[] forbidden, int a, int b, int x) {
int[] forbidden_idx = new int[6001]; // 因为还会有超过x的位置 避免多个if判断 所以直接扩大范围
for (int i : forbidden) { // 先记录禁止位置 ==1 代表位置禁止
forbidden_idx[i] = 1;
}
int[][] visit_idx = new int[6001][2]; // int[0][1]==1 在0这个位置 前进方向已经被访问
visit_idx[0][1] = 1;
// BFS 创建一个队列,队列中存储int[3]{now_idx, flag, step} // 现在位置、 是否可以倒退(0/1)、 当前步数
Queue<int[]> queue = new ArrayDeque<>();
queue.add(new int[]{0, 1, 0});
while (!queue.isEmpty()) {
int[] info = queue.poll();
if (x == info[0])
return info[2];
int[] temp = new int[]{info[0]+a, 1, info[2]+1};
if (temp[0] <= 6000 && forbidden_idx[temp[0]] == 0 && visit_idx[temp[0]][1] == 0) { // 起始点范围
queue.offer(temp);
visit_idx[temp[0]][1] = 1;
}
if (info[1] == 1 && info[0]-b >= 0){
temp = new int[]{info[0]-b, 0, info[2]+1};
if (forbidden_idx[temp[0]] == 0 && visit_idx[temp[0]][0] == 0){ // 是否为禁止点or访问点
queue.offer(temp);
visit_idx[temp[0]][0] = 1; // 访问点更新
}
}
}
return -1;
}
- 一个图中连通三元组的最小度数: 给一个无向图,一个 连通三元组 指的是 三个 节点组成的集合且这三个点之间 两两 有边;连通三元组的度数 是所有满足此条件的边的数目:一个顶点在这个三元组内,而另一个顶点不在这个三元组内。请你返回所有连通三元组中度数的 最小值 ,如果图中没有连通三元组,那么返回 -1 。
图思想: 仅仅需要记录下每个边的连接情况及 每个点的边数
public int minTrioDegree(int n, int[][] edges) {
int[][] graph = new int[n+1][n+1];
int[] edge_num = new int[n+1];
for (int[] edge : edges) { // <1,2> <2,1> 边均记为1 点1经过的边数目++
graph[edge[0]][edge[1]] = 1;
graph[edge[1]][edge[0]] = 1;
edge_num[edge[0]]++;
edge_num[edge[1]]++;
}
int res = Integer.MAX_VALUE;
for (int i = 1; i <= n; i++)
for (int j = i+1; j <= n; j++)
for (int k = j+1; k <= n; k++)
if (graph[i][j] + graph[i][k] + graph[j][k] == 3)
res = Math.min(res, edge_num[i]+edge_num[j]+edge_num[k]-6); // -6是因为三元组的边被统计了两遍
if (res == Integer.MAX_VALUE)
return -1;
else
return res;
}
六、9.4-9.8(二叉树、二分)
easy
- 最深叶节点的最近公共祖先:返回二叉树最深叶节点 的最近公共祖先: 也就是几个最深的叶子节点 最近的公共祖先
递归思想:返回二叉树最深叶节点 的最近公共祖先: 也就是几个最深的叶子节点 最近的公共祖先
public TreeNode lcaDeepestLeaves(TreeNode root) {
return dfs(root).getKey();
}
public Pair<TreeNode, Integer> dfs(TreeNode root) {
if (root == null)
return new Pair<>(null, 0);
Pair<TreeNode, Integer> left_pair = dfs(root.left);
Pair<TreeNode, Integer> right_pair = dfs(root.right);
int left_value = left_pair.getValue();
int right_value = right_pair.getValue();
if (left_value > right_value)
return new Pair<>(left_pair.getKey(), left_value+1);;
if (left_value < right_value)
return new Pair<>(right_pair.getKey(), right_value+1);;
return new Pair<>(root, left_value+1);
}
- 修车的最少时间:整数数组ranks表示机械工的能力值,能力值为ranks[i]的机械工在r*n^2分钟内修好n辆车。 返回修理工修理所有汽车的最少时间。修理工同时修理汽车。
枚举二分思想:枚举修车时间t 若t分钟内可以修理的车辆数>cars 那t就满足要求, 也就是找一个最小时间t 能修理的车辆数最多,多于cars
public long repairCars(int[] ranks, int cars) {
Arrays.sort(ranks);
long left = 1, right = (long)cars*cars*ranks[ranks.length-1];
while (left < right){
long mid = (right + left) / 2;
long now_cars = 0; // 必须是long类型,因为后面相加可能会超出界
for (int rank : ranks)
now_cars += (long)Math.sqrt(mid / rank);
if(now_cars >= cars)
right = mid;
else
left = mid+1; // 这个是小于 所要修理的车, 不存在等于 所有一定要+1,要不然会死循环 比如剩下两个数的时候。
}
return left;
}
}
每日一题
22.7.16 无重复字符的最长子串(滑动窗口)(HashMap类)
给定一个字符串s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例2:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
错误思想 :通过判断end和begin是否相等来移动end和begin,但是会出现“abcbb”这种a与后面两个b均不同 但是不是无重复字符。无法判断中间出现重复的情况!
public int lengthOfLongestSubstring(String s) {
HashMap<Character, Integer> map = new HashMap<>();
int left = 0;
int res = 0;
// 将子串放到map中,如果新来的在里面,更新左边 否则++
for(int i = 0; i < s.length(); i++) {
if(map.containsKey(s.charAt(i)))
left = Math.max(left, map.get(s.charAt(i))+1);
map.put(s.charAt(i), i);
res = Math.max(res, i-left+1);
}
return res;
}
滑动窗口:利用map工具,只需要注意更新left就可以。
注意:( abba情况 ) left更新,当到达第二个b时,left记录为1+1,但是到达下一个a时,会记录为0+1,第一个a已经不再有效长度里面了!所以每次left应该取max。
22.7.19 最长回文子串(dp、中心扩散法)
给你一个字符串s,找到 s中最长的回文子串。
示例1:
输入: s = “babad”
输出: “bab”
解释:“aba” 同样是符合题意的答案。
两种思想 时间都是O( n 2 n^2 n2) 空间分别为O( n 2 n^2 n2)、O(1)
1.动态规划:回文具有天然的 状态转移 性质,去掉头尾之后,剩下的依然是回文。
转移方向:从短字符串向长字符串移动;初始化dp[i][i] = true
;转移方程dp[i][j] = dp[i+1][j-1]&&s[i]=s[j]
。
初始化res默认为s的第一位字符。 大循环为字串的长度,由短向长,小循环i为左端,
i+len-1
为右端点:当left和right相等时,判断如果相差一位,那就是回文,否则判断它的字串为回文就是回文;不相等则false。=>当true且字串长度大于max更新最长字串。
public static String longestPalindromeDynamic(String s) {
int n = s.length();
String res = String.valueOf(s.charAt(0)) ;
int max = 0;
boolean[][] dp = new boolean[n][n];
for(int i = 0; i < n; i++)
dp[i][i] = true;
for(int len = 2; len <= n; len++) //每次字串的长度为最外层的大循环
for(int i = 0; i + len - 1 < n; i++) {//固定长度来移动
int right = i + len - 1;
if(s.charAt(i) == s.charAt(right))
if(i+1 == right)
dp[i][right] = true;
else
dp[i][right] = dp[i+1][right-1];
else
dp[i][right] = false;
if(dp[i][right] && max < len) {
max = len;
res = s.substring(i, right+1);
}
}
return res;
}
2.中心扩散法:枚举所有的「回文中心」并尝试「扩展」
public static String longestPalindromeSpread(String s) {
int max = 0;
String res = "";
for(int i = 0;i < s.length(); i++) {// 从一个点向两边扩散,分奇偶情况
int len1 = spreadLenth(i, i+1, s);
int len2 = spreadLenth(i, i, s);
max = Math.max(len1, len2);
if(max > res.length()) {
int left = max%2==0 ? max/2-1 : max/2;
res = s.substring(i-left, i+max/2+1);
}
}
return res;
}
public static int spreadLenth(int left, int right, String s) {// 输入左右初始边界,返回扩散的最大长度
int ans = 1;
while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
ans = right - left + 1;
left--;
right++;
}
return ans;
}
22.7.20 盛最多水的容器(双指针)
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
示例1:
输入: [1,8,6,2,5,4,8,3,7]
输出: 49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
双指针:要明白容量是由左右两端最小值*宽度得到的。所以每次移动的是最小的指针,让他做出改变!每次先进行容量计算,这个时候宽度是最宽的(此时还没有向中间移动),所以直接用最小高度相乘即可。 注意:不需要一直保留最大容积的位置、高度
public static int maxArea(int[] height) {//左右并进 比较左右大小,从小的一头移动
int i = 0;
int j = height.length-1;
int maxContain = Math.min(height[i], height[j]) * (j-i);
while(i < j) {
maxContain = Math.max(maxContain, Math.min(height[i], height[j]) * (j-i));
if(height[i] <= height[j])
i++;
else
j--;
}
return maxContain;
}
22.7.23 环形链表 II(双指针)
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
经典找环形链表入环点题:设置快慢指针!快指针速度=2慢指针速度
首先记录快慢指针相遇点 此时有slow:x+y ,fast: x+z+2y(假设多走了仅一圈),又由于快指针两倍速于慢指针,所以x=z ==>此时快慢指针均以慢指针速度运动,快指针从头出发,慢指针从相遇点出发,再次相遇即为入口节点。
public ListNode detectCycle(ListNode head) {
//对于重合情况:第一次重合时,用图描述可以知道,在此处出发;同时另一个点从起点出发,再次相遇极为环的起点
if(head == null || head.next == null)
return null;
ListNode slow = head.next;
ListNode quick = head.next.next;
while(slow != quick){
if(quick==null || quick.next==null)
return null;
slow = slow.next;
quick = quick.next.next;
}
//有环了
quick = head;
while(slow != quick){
quick = quick.next;
slow = slow.next;
}
return slow;
}
22.7.24 每日温度(单调栈)
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
输入:temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
思想 单调栈:O(n)
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
首先考虑两点。1.单调栈中的元素存放:本题存放的是下标,可以很方便的计算出所差天数。2.单调增栈or减栈?本题是减栈(栈底到栈顶递减)。考虑完之后,对情况进行分类知道只有非空栈且栈顶元素的温度小于当前元素的温度才出栈,当此次循环栈中元素都不满足上述情况时,才入栈。
public int[] dailyTemperatures(int[] temperatures) {
// !找大的元素!,利用单调栈,存入下标。 当栈不为空且栈顶元素小于遍历元素:退栈,存入
Stack<Integer> stack = new Stack<Integer>();
int[] res = new int[temperatures.length];
for(int i = 0; i < temperatures.length; i++) {
while(!stack.empty() && temperatures[stack.peek()] < temperatures[i]) {
int index = stack.pop();
res[index] = i - index;
}
stack.push(i); //这里要注意 是要将栈内全部小于i的温度都退栈后 才能入栈
}
return res;
}
22.7.25 接雨水(DP、单调栈、双指针)
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例1:
输入: height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
三种思想 时间都是O(n) 空间分别为O(n)、O(n)、O(1)
这里面单调栈的思想是一行一行的、而其它两种方法均是一个纵列上计算ans。
1.动态规划:根据暴力得出来的动态规划:求每一快柱子所能接的雨水量即求min(左边最大,右边最大)-该柱子高度。
关键就是表示出i位置的左边最大、右边最大。故先用两次循环来得出每个点的leftMax(左到右记录)、rightMax(右到左记录)数组然后就可以直接计算了
leftMax[0] = height[0];
for(int i = 1; i < n; i++)
leftMax[i] = Math.max(height[i], leftMax[i-1]);
rightMax[n-1] = height[n-1];
for(int i = n-2; i >= 0; i--)
rightMax[i] = Math.max(height[i], rightMax[i+1]);
2.单调栈:
栈中存储的是下标,由大到小(底->顶)这样一出现比顶大的,代表凹槽来了,可以存水了。
所以有 不为空且大于栈顶元素,栈顶元素出栈,计算min(新栈顶,当前元素)-出栈元素 * 当前元素pos-新栈顶pos
理解:该方法是一行计算的 当出现大的时候,就是找到右边相对最高的元素了,出栈之后的新栈顶就是左边相对最高的元素。这只是相对最高!所以计算的仅仅是低到高这段距离的行
public static int trap(int[] height) {
Stack<Integer> stack = new Stack<Integer>();
int ans = 0;
for(int i = 0; i < height.length; i++) {
while(!stack.isEmpty() && height[stack.peek()]<height[i]) {
int cur = stack.pop();
if(stack.isEmpty())
break;
int h = Math.min(height[stack.peek()], height[i]) - height[cur];
int w = i - stack.peek() - 1;
ans += h * w;
}
stack.push(i);
}
return ans;
}
3.双指针:最快的。同1一样思想,不过找到的左右max是仅用int存储。所以需要左右两端同时遍历,使用leftMax、rightMax实时更新左右两端的max。比较两端大小,从小的一端入手(肯定是最低高度)
public static int trap2(int[] height) {
//双指针 左右两边一起动,那边小从哪边入手 始终有leftMax、rightMax两个记录经历过的左右两边最大值
int ans = 0;
int leftMax = 0, rightMax = 0;
int left = 0, right = height.length-1;
while(left < right) {
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
if(height[left] < height[right]) {//计算的是当前left、right所能接的雨水数量
ans += leftMax - height[left];
left++;
}
else {
ans += rightMax - height[right];
right--;
}
}
return ans;
}
22.7.26 柱状图中最大的矩形(单调栈)
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例1:
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
示例2:
输入: heights = [2,4]
输出: 4
本题时找左右两边小的位置,这样代表这个柱子的高度可以在这之间构成矩形的高。
单调栈:栈中存储的是下标,由小到大(底->顶)碰到小的元素时开始结算 凸形状开始结算。
注意 1.本题要确保栈中元素全部出栈,所以要在末尾添加一个最小值:0(之前时找最大构成容器,没有最大代表就构不成了。本题栈中全部元素均有用);
2.当出现比栈顶小的元素时但是栈中只有一个元素,代表左端没有比栈顶更小的元素了,此时的w就是当前比较的位置i
当遇到小的元素时,出栈,新栈顶即为左端最小,当前比较元素即为右边最小。h=出栈元素 w=当前小元素pos-新栈顶pos-1
public static int largestRectangleArea(int[] heights) {
int ans = 0;
Stack<Integer> stack = new Stack<Integer>();
int[] tempHeight = Arrays.copyOf(heights, heights.length+1); //第二个参数为新数组长度,所以不需事先定义tempHeight的长度
for(int i = 0; i < tempHeight.length; i++) {
while(!stack.isEmpty() && tempHeight[stack.peek()] > tempHeight[i]) {
int h = tempHeight[stack.pop()];
int w;
if(stack.isEmpty()) //代表出栈元素左边没有比他小的了 宽度即为i(在数组首部加1也可以挽回)
w = i;
else
w = i - stack.peek() - 1;
ans = Math.max(ans, w*h);
}
stack.push(i);
}
return ans;
}
22.8.3 合并K个升序链表(归并分而治之、优先级队列)(PriorityQueue类、比较器)
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
两种思想
1.分而治之、链表两两合并:效用归并排序的思想,将链表数组分为两部分,两两合并。
public ListNode mergeKLists (ListNode[] lists) {
if (lists.length == 0) return null;
return tempMerge(lists, 0, lists.length-1);
}
public ListNode tempMerge(ListNode[] lists, int left, int right){
if (left >= right) return lists[left];
ListNode l1 = tempMerge(lists, left, left+(right-left)/2);
ListNode l2 = tempMerge(lists, (right+left)/2+1, right);
return merge(l1, l2);
}
public ListNode merge(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) {
l1.next = merge(l1.next, l2);
return l1;
} else {
l2.next = merge(l1, l2.next);
return l2;
}
}
2.优先级队列
首先是比较器,定义一个单独的对象比较器,继承自Comparator接口,实现compare()方法;然后是优先级队列,PriorityQueue是默认是通过小顶堆来实现优先级队列的,也可以指定Comparator自定义实现队列的优先级。
我们需要维护当前每个链表没有被合并的元素的最前面一个,k 个链表就最多有 k 个满足这样条件的元素,每次在这些元素里面选取val 属性最小的元素合并到答案中。在选取最小元素的时候,我们可以用优先队列来优化这个过程。
//比较器
class MyComparator implements Comparator<ListNode>{
@Override
public int compare(ListNode l1, ListNode l2) {
//返回负数 不交换顺序
return l1.val-l2.val;
}
}
public static ListNode mergeKLists (ListNode[] lists) {
if(lists.length == 0)
return null;
PriorityQueue<ListNode> queue = new PriorityQueue<ListNode>(new MyComparator());
//将ListNode放入队列中,每次排出最下的那个
for(ListNode list : lists)
if(list!=null) queue.add(list);
ListNode head = new ListNode();
ListNode cur = head;
while(!queue.isEmpty()) {
ListNode temp = queue.poll();
cur.next = temp;
cur = cur.next;
if(temp.next != null) queue.add(temp.next);
}
return head.next;
}
22.8.11(回溯合集,组合问题:N个数里面按一定规则找出k个数的集合)
void backtracking(参数) {
//递归出口
if (终止条件) {
存放结果;
return;
}
//回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
//backtracking就是纵向遍历,for循环就是横向遍历
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
public void f(List<Integer> list, int n, int k, int i){
if(list.size() == k){
ans.add(new ArrayList<>(list));
return ;
}
for(; i <= n; i++){
list.add(i);
f(list, n, k, i+1);
list.remove(list.size() - 1);
}
}
上下两个题均为从 某个范围内找符合某个规则的k个数的组合 只不过上面那个是没有规则要求,而下面的是要符合和为n的要求。
所以上面那个是最原始的递归版本,而下面的根据要满足和为n的要求,在递归出口要加上sum的判断;在for中进行剪枝操作:后续无法凑成和 以及 list已经到达容量k 均直接break掉。
public void f(List<Integer> list, int k, int n, int i, int sum){
if(list.size() == k && sum == n){
ans.add(new ArrayList<>(list));
return ;
}
for(; i <= 9; i++){
if((n - sum) < i || list.size()>k)
break;
list.add(i);
f(list, k, n, i+1, sum+i);
list.remove(list.size()-1);
}
}
-----------------------------😐 --------------------------------😐-------------------------------
组合总和III
找出所有相加之和为 n 的k 个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
组合总和
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
public void f(List<Integer> list, int[] candidates, int target, int sum, int i){
if(sum == target){
ans.add(new ArrayList<>(list));
return;
}
for(; i < candidates.length; i++){
if((target-sum) < candidates[i])
break;
list.add(candidates[i]);
f(list, candidates, target, sum+candidates[i], i);//因为可重复 所以下一次还是从i开始
list.remove(list.size()-1);
}
}
这两个题均是从数组中找出组合,使和为target。但是下题中的 数组是有重复数字 并且 数组中每个数字仅能使用一次
组合总和II
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
本题要避免出现重复的组合,横向(同一层级不能出现相同的元素)即
1
/ \
2 2 这种情况不会发生 但是却允许了不同层级之间的重复即:
/ \
5 5
1
/
2 这种情况确是允许的
/
2
if(i!=0 && candidates[i]==candidates[i-1] && i!=begin)
首先判断 是否是相同的元素,通过i
和i-1
是否相等判断;然后下一步判断 是否处于同一层级上 ,for循环中是横向的遍历,在元素相等时,通过判断i
和begin
不相等,可以得出是在同一层(相等代表这是刚进入这一层的,i
还没有++)
List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<Integer> list = new ArrayList<>();
Arrays.sort(candidates);
f(list, candidates, target, 0, 0);
return ans;
}
public void f(List<Integer> list, int[] candidates, int target, int sum, int begin){
if(sum == target){
ans.add(new ArrayList<>(list));
return;
}
for(int i = begin; i < candidates.length; i++){
if((target-sum) < candidates[i])
break;
// 对于删选122 和125、125不重复的情况,先判断和前一个数是否相等 然后判断i是否为同一个i
if(i!=0 && candidates[i]==candidates[i-1] && i!=begin)
continue;
list.add(candidates[i]);
f(list, candidates, target, sum+candidates[i], i+1);
list.remove(list.size()-1);
}
}
22.8.31(dp合集,0-1背包问题:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。)
二维数组表示dp[i][j]:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
。比较放物品和不放物品情况下,那个得到的价值最大。不放物品:dp[i - 1][j]
;放物品:dp[i - 1][j - weight[i]] + value[i])
(当第i件物品重量>j时,直接为不放物品。)
初始化:dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。故dp[0][:j]=0,dp[0][j:]=value[0]
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
一维数组表示dp[j]:容量为j的背包,所背的物品价值可以最大为dp[j]。(二维的每次都是将dp[i-1]拷贝到dp[i]这一层上,仅仅是上一层的替换)
递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
初始化:dp[0]=0]
,背包容量为0所背的物品的最大价值就是0。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}//倒序遍历是为了保证物品i只被放入一次!
一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
示例:
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
本题要理解为m、n代表两个容量,所以等价于常规背包问题是三维数组。每一次计算第i个背包,需要计算它的0和1的容量。然后根据每个字符串的‘0’,‘1’数目来更新dp,因为采用的是降维数组,所以一定要 逆序遍历.
public int findMaxForm(String[] strs, int m, int n) {
//dp[i][j]是容量0、1 而str才是物品
int[][] dp = new int[m+1][n+1];
for(int i = 0; i < strs.length; i++){
int zeroNum = 0;
int oneNum = 0;
for(char c : strs[i].toCharArray())
if(c == '0')
zeroNum++;
else
oneNum++;
for(int j = m; j >= zeroNum; j--)
for(int k = n; k >= oneNum; k--)
dp[j][k] = Math.max(dp[j][k], dp[j-zeroNum][k-oneNum]+1);
}
return dp[m][n];
}
Pytorch知识
transforms详解
1.介绍
transforms在计算机视觉工具包torchvision下
torchvision.transforms: 常用的图像预处理方法,提高泛化能力
torchvision.datasets : 常用数据集的dataset实现,MNIST,CIFAR-10,ImageNet等
torchvision.model : 常用的模型预训练,AlexNet,VGG, ResNet,GoogLeNet等
2.方法合集:裁剪Crop、翻转和旋转Flip and Rotation、图像变换
裁剪
(1)、随机裁剪:torchvision.transforms.RandomCrop(size, padding=None, pad_if_needed=False, fill=0, padding_mode='constant')
功能:依据给定的size随机裁剪; 参数: size- (sequence or int),若为sequence,则为(h,w),若为int,则(size,size)
(2)、中心裁剪:torchvision.transforms.CenterCrop(size)
功能:依据给定的size从中心裁剪 参数: size- (sequence or int),
(3)、随机长宽比裁剪:torchvision.transforms.RandomResizedCrop(size, scale=(0.08, 1.0), ratio=(0.75, 1.3333333333333333), interpolation=2)
功能:随机大小,随机长宽比裁剪原始图片,最后将图片resize到设定好的size。scale指定裁剪的大小为:原面积*scale;ratio指定宽高比
翻转和旋转
(1)、依概率p水平翻转:class torchvision.transforms.RandomHorizontalFlip(p=0.5)
功能:依据概率p对PIL图片进行水平翻转 参数: p- 概率,默认值为0.5
(2)、随机旋转:torchvision.transforms.RandomRotation(degrees, resample=False, expand=False, center=None)
功能:依degrees随机旋转一定角度 参数: degress- (sequence or float or int) ,若为单个数,如 30,则表示在(-30,+30)之间随机旋转 若为sequence,如(30,60),则表示在30-60度之间随机旋转
图像变换
(1)、resize:torchvision.transforms.Resize(size, interpolation=2)
功能:功能:重置图像分辨率 参数: size- If size is an int, if height > width, then image will be rescaled to (size * height / width, size),所以建议size设定为h * w
(2)、转为tensor:torchvision.transforms.ToTensor
功能:将PIL Image或者 ndarray 转换为tensor,并且归一化至[0-1] 注意事项:归一化至[0-1]是直接除以255,若自己的ndarray数据尺度有变化,则需要自行修改。
(3)、标准化:torchvision.transforms.Normalize(mean, std)
功能:对数据按通道进行标准化,即先减均值,再除以标准差,注意是 hwc
3.运行机制
采用transforms.Compose(),将一系列的transforms有序组合,实现时按照这些方法依次对图像操作。
展示:
# 该标准化数据为肺炎识别所用的数据
# 训练集的操作 :随机裁剪->随机旋转->水平翻转->中心裁剪->转tensor并标准化
torchvision.transforms.Compose([
# 功能:随机长宽比裁剪原始图片, 表示随机crop出来的图片会在的0.8倍至1.1倍之间,然后resize成size大小的图片
torchvision.transforms.RandomResizedCrop(size=300, scale=(0.8, 1.1)),
torchvision.transforms.RandomRotation(degrees=10), # 在(-degrees,degrees)之间随即旋转
torchvision.transforms.RandomHorizontalFlip(), # 水平翻转
torchvision.transforms.CenterCrop(256), # 根据给定的size从中心裁剪,size-若为sequence,则为(h,w),若为int,则(size,size)
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
# 测试集的操作 : 改变图片大小->中心裁剪->转tensor并标准化
torchvision.transforms.Compose([
torchvision.transforms.Resize(300),
torchvision.transforms.CenterCrop(256),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])