A动态规划
三角形 方格取数*
上升子序列 怪盗基德的滑翔翼 登山 合唱队形 友好城市
拦截导弹 导弹防御系统 *
最长公共上升子序列 *
背包 菜药 装箱问题 宠物小精灵之收服* 基础
数字组合 买书 货币系统 简化货币系统 组合方案
庆功会 多重背包问题 III 多重背包
状态机
状压
区间
树形
数位
单调队列优化
斜率优化

B搜索
Flood Fill 池塘计数 城堡问题 山峰和山谷

基础代码

  1. import java.util.*;
  2. class Main{
  3. public static void main(String[] args){
  4. Scanner sc = new Scanner(System.in);
  5. int n = sc.nextInt();
  6. }
  7. }

动态规划

数字三角形模型

摘花生

最低通行费

方格取数

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 基本思路:dp[x1][y1][x2][y2]表示同时走到两个坐标的最大值
  5. 由于是同时在走,存在关系:x1+y1 = x2+y2 = d,其中d是横纵坐标的相加的和
  6. 因此状态可以表示为 dp[x1][x2][d]
  7. 属性:最大值
  8. 涉及的之前的状态划分:
  9. dp[x1-1][x2-1][d-1],dp[x1-1][x2][d-1],dp[x1][x2-1][d-1],dp[x1][x2][d-1]
  10. */
  11. public static void main(String[] args){
  12. Scanner sc = new Scanner(System.in);
  13. int N = sc.nextInt();
  14. int[][] mat = new int[N+1][N+1];
  15. while(true){
  16. int r = sc.nextInt(),c = sc.nextInt(),v = sc.nextInt();
  17. if(r == 0 && c == 0 && v == 0) break;
  18. mat[r][c] = v;
  19. }
  20. int[][][] dp = new int[N*2+1][N+1][N+1];
  21. for(int d = 2;d <= 2*N;++d){
  22. for(int r1 = 1;r1 <= N;++r1){
  23. for(int r2 = 1; r2 <= N;++r2){
  24. int c1 = d-r1,c2 = d-r2;
  25. if(c1 < 1 || c1 > N || c2 < 1 || c2 > N) continue; // 注意范围!!!!!!!!
  26. if(r1 == r2){ // 同时走的两条路径出现交点,但是只能取一次数
  27. dp[d][r1][r2] = mat[r1][c1];
  28. }else{
  29. dp[d][r1][r2] = mat[r1][c1]+mat[r2][c2]; // 同时走的两条路径没有交点,取到2个数字
  30. }
  31. int tmp1 = Math.max(dp[d-1][r1-1][r2],dp[d-1][r1][r2-1]);
  32. int tmp2 = Math.max(dp[d-1][r1-1][r2-1],dp[d-1][r1][r2]);
  33. dp[d][r1][r2] += Math.max(tmp1,tmp2); // 从可能的四个前置状态中选出最大值
  34. }
  35. }
  36. }
  37. System.out.println(dp[2*N][N][N]);
  38. }
  39. }

传纸条

最长上升子序列模型

  1. 基本的算法:1)动态规划求解(n^2 2)二分求解(n*logn)
  2. 难点:分析题意联想到该模型

怪盗基德的滑翔翼

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 基本思路:求正向的最长下降(严格)子序列和反向的最长下降(严格)子序列
  5. 评论区问题: 反向的最长下降子序列 是否等价于 正向的最长上升子序列 ?
  6. */
  7. public static void main(String[] args){
  8. Scanner sc = new Scanner(System.in);
  9. int k = sc.nextInt();
  10. int[] arr = new int[101];
  11. int[] high = new int[101],low = new int[101];
  12. while(k-- > 0){
  13. int n = sc.nextInt();
  14. for(int i = 0;i < n;++i) arr[i] = sc.nextInt();
  15. high[0] = 1; low[0] = 1;
  16. int res = 0;
  17. for(int i = 1;i < n;++i){
  18. high[i] = 1; low[i] = 1;
  19. for(int j = 0;j < i;++j){
  20. if(arr[i] > arr[j]) high[i] = Math.max(high[j]+1,high[i]);
  21. if(arr[i] < arr[j]) low[i] = Math.max(low[j]+1,low[i]);
  22. }
  23. res = Math.max(res,Math.max(high[i],low[i]));
  24. }
  25. System.out.println(res);
  26. }
  27. }
  28. }

登山

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 分析非常关键:
  5. 1)按照顺序来浏览这些景点 => 获取一个子序列
  6. 2)不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了 => 子序列必定是先严格单调递增后严格单调下降(山峰状)
  7. */
  8. public static void main(String[] args){
  9. Scanner sc = new Scanner(System.in);
  10. int N = sc.nextInt();
  11. int[] low = new int[N],arr = new int[N],high = new int[N];
  12. for(int i = 0;i < N;++i) arr[i] = sc.nextInt();
  13. // high[i]表示以arr[i]结尾的最长上升子序列长度
  14. Arrays.fill(high,1);
  15. for(int i = 1;i < N;++i){
  16. for(int j = 0;j < i;++j){
  17. if(arr[i] > arr[j]){
  18. high[i] = Math.max(high[i],high[j]+1);
  19. }
  20. }
  21. }
  22. // low[i]表示以arr[i]开头的最长下降子序列长度
  23. Arrays.fill(low,1);
  24. for(int i = N-2;i >= 0;--i){
  25. for(int j = i+1;j < N;++j){
  26. if(arr[i] > arr[j]){
  27. low[i] = Math.max(low[i],low[j]+1);
  28. }
  29. }
  30. }
  31. int res = 0;
  32. for(int i = 0;i < N;++i){
  33. res = Math.max(res,low[i] + high[i] -1);
  34. }
  35. System.out.println(res);
  36. }
  37. }

合唱队形

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 题意:求最长的子序列,满足先严格单调增后严格单调减
  5. */
  6. public static void main(String[] args){
  7. Scanner sc = new Scanner(System.in);
  8. int N = sc.nextInt();
  9. int[] low = new int[N],arr = new int[N],high = new int[N];
  10. for(int i = 0;i < N;++i) arr[i] = sc.nextInt();
  11. // high[i]表示以arr[i]结尾的最长上升子序列长度
  12. Arrays.fill(high,1);
  13. for(int i = 1;i < N;++i){
  14. for(int j = 0;j < i;++j){
  15. if(arr[i] > arr[j]){
  16. high[i] = Math.max(high[i],high[j]+1);
  17. }
  18. }
  19. }
  20. // low[i]表示以arr[i]开头的最长下降子序列长度
  21. Arrays.fill(low,1);
  22. for(int i = N-2;i >= 0;--i){
  23. for(int j = i+1;j < N;++j){
  24. if(arr[i] > arr[j]){
  25. low[i] = Math.max(low[i],low[j]+1);
  26. }
  27. }
  28. }
  29. int res = 0;
  30. for(int i = 0;i < N;++i){
  31. res = Math.max(res,low[i] + high[i] -1);
  32. }
  33. System.out.println(N-res);
  34. }
  35. }

友好城市

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 要求:选出最长的严格单调的子序列,子序列中每个元素是二元组(a,b)
  5. 显然:只要保证元组的单调性,一定不会出现相加的情况
  6. 方法:可以转换为LIS模型
  7. */
  8. public static void main(String[] args){
  9. Scanner sc = new Scanner(System.in);
  10. int N = sc.nextInt();
  11. int[][] point = new int[N][2];
  12. for(int i = 0;i < N;++i){
  13. point[i][0] = sc.nextInt(); point[i][1] = sc.nextInt();
  14. }
  15. Arrays.sort(point,(o1,o2)->{ // 保证一维有序
  16. return Integer.compare(o1[0],o2[0]);
  17. });
  18. int[] dp = new int[N];
  19. int res = 0;
  20. Arrays.fill(dp,1);
  21. for(int i = 1;i < N;++i){
  22. for(int j = 0;j < i;++j){
  23. if(point[i][1] > point[j][1]){
  24. dp[i] = Math.max(dp[i],dp[j]+1);
  25. }
  26. }
  27. res = Math.max(res,dp[i]);
  28. }
  29. System.out.println(res);
  30. }
  31. }

最大上升子序列和

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 要求:求所有上升子序列的和的最大值,由于题目数据范围都是正整数
  5. dp[i]:表示以arr[i]结尾的最大上升子序列和
  6. */
  7. public static void main(String[] args){
  8. Scanner sc = new Scanner(System.in);
  9. int N = sc.nextInt();
  10. int[] arr = new int[N],dp = new int[N];
  11. for(int i = 0;i < N;++i){
  12. arr[i] = sc.nextInt(); dp[i] = arr[i];
  13. }
  14. int res = arr[0];
  15. for(int i = 1;i < N;++i){
  16. for(int j = 0;j < i;++j){
  17. if(arr[i] > arr[j]){
  18. dp[i] = Math.max(dp[j]+arr[i],dp[i]);
  19. }
  20. }
  21. res = Math.max(res,dp[i]);
  22. }
  23. System.out.println(res);
  24. }
  25. }

拦截导弹

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 问题1:求最长不上升子序列?
  5. 问题2:最少需要多少个不上升子序列可以覆盖整个序列
  6. 解决思路:
  7. 贪心思想:从前往后扫描每个数,对于每个数
  8. 情况1:现有所有子序列结尾小于当前数,则创建新子序列
  9. 情况2:将当前数放到所有大于等于当前数的子序列结尾中最小的那个子序列后面
  10. 最终答案就是子序列个数
  11. 注意:问题1和2都可以优化为 n*log(n)算法
  12. 问题2的解决流程与求最长上升子序列的贪心解法是一致的。
  13. 背后蕴含着离散数学的原理:狄尔沃斯定理(Dilworth's theorem)亦称偏序集分解定理,
  14. 贪心思路证明:
  15. 证明A = B 等价于 证明 A >= B 且 A <= B 其中A表示贪心算法得到的序列个数,B表示最优解
  16. 显然B >= A成立
  17. 证明:A <= B
  18. 采用调整法证明,假设存在不同于贪心法得到的序列的最优解,可以通过调整序列得到贪心法序列,即最优解经过调整后不会增加长度
  19. 可以得到贪心解
  20. */
  21. public static void main(String[] args){
  22. Scanner sc = new Scanner(System.in);
  23. int[] arr = new int[(int)(1e3+10)];
  24. int n = 0;
  25. while(sc.hasNext()){
  26. arr[n++] = sc.nextInt();
  27. }
  28. int[] dp = new int[n],up = new int[n];
  29. /*求最长不上升子序列*/
  30. Arrays.fill(dp,1);
  31. int res1 = 1;
  32. for(int i = 1;i < n;++i){
  33. for(int j = 0;j < i;++j){
  34. if(arr[i] <= arr[j]){
  35. dp[i] = Math.max(dp[i],dp[j]+1);
  36. }
  37. }
  38. res1 = Math.max(res1,dp[i]);
  39. }
  40. /*求最少覆盖的序列个数*/
  41. // up是严格递增的,找到第一个大于等于当前元素的末尾等价于找到大于等于当前元素的最小末尾序列元素
  42. int idx = 0;
  43. for(int i = 0;i < n;++i){
  44. int k = 0;
  45. while(k < idx && up[k] < arr[i]) ++k;
  46. up[k] = arr[i];
  47. }
  48. System.out.printf("%d\n",res1);
  49. System.out.printf("%d\n",idx);
  50. }
  51. }

导弹防御系统

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 问题:最少需要多少个上升/下降子序列覆盖原始序列?
  5. 基本思路:贪心策略(参考拦截导弹的贪心思路)+dfs枚举
  6. 方法:DFS求最小步数: 1)迭代加深 2)更新全局最小值
  7. bfs相比较dfs的缺点: 1)搜索需要大量空间(dfs只存路径) 2)不方便剪枝
  8. */
  9. public static int N = 51;
  10. public static int[] arr = new int[N];
  11. public static int[] up = new int[N],down = new int[N];
  12. public static int n,res;
  13. /*当前数,上升子序列个数,下降子序列个数*/
  14. public static void dfs(int idx,int upNum,int downNum){
  15. /*剪枝,不然超时*/
  16. if(upNum + downNum >= res) return;
  17. if(idx == n){
  18. res = Math.min(res,downNum + upNum);
  19. return;
  20. }
  21. /*情况1:贪心法构造上升子序列*/
  22. int k = 0;
  23. while(k < upNum && up[k] >= arr[idx]) ++k;
  24. int t = up[k];
  25. up[k] = arr[idx];
  26. if(k == upNum) dfs(idx+1,upNum+1,downNum);
  27. else dfs(idx+1,upNum,downNum);
  28. up[k] = t;
  29. /*情况2:贪心法构造下降子序列*/
  30. k = 0;
  31. while(k < downNum && down[k] <= arr[idx]) ++k;
  32. t = down[k];
  33. down[k] = arr[idx];
  34. if(k == downNum) dfs(idx+1,upNum,downNum+1);
  35. else dfs(idx+1,upNum,downNum);
  36. down[k] = t;
  37. }
  38. public static void main(String[] args){
  39. Scanner sc = new Scanner(System.in);
  40. while(true){
  41. n = sc.nextInt(); res = Integer.MAX_VALUE;
  42. if(n == 0) break;
  43. for(int i = 0;i < n;++i) arr[i] = sc.nextInt();
  44. dfs(0,0,0);
  45. System.out.println(res);
  46. }
  47. }
  48. }

最长公共上升子序列

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 最长公共上升子序列解题思路:
  5. dp[i][j]:a[1 ~ i]和b[1 ~ j]中以b[j]结尾的公共上升子序列的集合(通过结尾元素划分集合)
  6. 状态转移:
  7. 1)不包含有a[i], dp[i-1][j]
  8. 2)包含a[i], 当a[i] == b[j], 则 max(dp[i-1][1~(j-1)]+1) 当a[i] > b[j] (严格递增)
  9. dp[i][j]为上面两种情况较大值
  10. */
  11. public static void main(String[] args){
  12. Scanner sc = new Scanner(System.in);
  13. int N = sc.nextInt();
  14. int[] a = new int[N+1],b = new int[N+1];
  15. for(int i = 1;i <= N;++i) a[i] = sc.nextInt();
  16. for(int i = 1;i <= N;++i) b[i] = sc.nextInt();
  17. int res = 0;
  18. int[][] dp = new int[N+1][N+1];
  19. for(int i = 1;i <= N;++i){
  20. int maxv = 1;
  21. for(int j = 1;j <= N;++j){
  22. dp[i][j] = dp[i-1][j]; // 情况1:不包含a[i]
  23. if(a[i] == b[j]) dp[i][j] = Math.max(dp[i][j],maxv); // 情况2:包含a[i]
  24. if(a[i] > b[j]) maxv = Math.max(maxv,dp[i-1][j]+1); //维护dp[i-1][(1-j-1)]的最大值(注意该行与上一行顺序不能颠倒)
  25. res = Math.max(res,dp[i][j]);
  26. }
  27. }
  28. System.out.println(res);
  29. }
  30. }

3780 构造数组(思维训练)

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 基本思想:
  5. 题目可以转换为满足要求的和最大的单峰函数。
  6. 考虑:枚举数组的每个位置作为峰值,求出当前位置作为峰值的单峰函数最大值。
  7. 令位置k作为峰值,则需要求[0,k]的最大和leftSum与[k,n-1]的最大和rightSum
  8. 最终和等于:leftSum[k]+rightSum[k]-nums[k]
  9. 优化点:如何快速确定每个位置k作为峰值,其单峰函数最大值。
  10. 单调栈优化:leftSum[k] = (k-j)*nums[k]+leftSum[j] 其中j为第一个小于nums[k]的数字的下标。
  11. 通过上面的状态转移我们可以利用其他位置的计算结果。
  12. */
  13. public static void main(String[] args){
  14. Scanner sc = new Scanner(System.in);
  15. int n = sc.nextInt();
  16. long[] nums = new long[n];
  17. for(int i = 0;i < n;++i){
  18. nums[i] = sc.nextLong();
  19. }
  20. Stack<Integer> st = new Stack();
  21. int[] leftIndex = new int[n];
  22. Arrays.fill(leftIndex,-1);
  23. // 左边第一个小于该元素的坐标
  24. for(int i = n-1;i >= 0;--i){
  25. while(!st.isEmpty() && nums[st.peek()] > nums[i]){ // 递增栈
  26. int idx = st.pop();
  27. leftIndex[idx] = i;
  28. }
  29. st.push(i);
  30. }
  31. // 右边第一个小于该元素坐标
  32. st.clear();
  33. int[] rightIndex = new int[n];
  34. Arrays.fill(rightIndex,n);
  35. for(int i = 0;i < n;++i){
  36. while(!st.isEmpty() && nums[st.peek()] > nums[i]){
  37. int idx = st.pop(); rightIndex[idx] = i;
  38. } // 递增栈
  39. st.push(i);
  40. }
  41. /*当前位置k作为峰值,区间[0,k]的和*/
  42. long[] leftSum = new long[n];
  43. for(int i = 0;i < n;++i){
  44. int idx = leftIndex[i];
  45. leftSum[i] = (i-idx)*nums[i];
  46. long tmp = idx == -1 ? 0 : leftSum[idx];
  47. leftSum[i] += tmp;
  48. }
  49. /*当前位置k作为峰值,区间[k,n-1]的和*/
  50. long[] rightSum = new long[n];
  51. for(int i = n-1;i >= 0;--i){
  52. int idx = rightIndex[i];
  53. rightSum[i] = (idx-i)*nums[i];
  54. long tmp = idx == n ? 0 : rightSum[idx];
  55. rightSum[i] += tmp;
  56. }
  57. long res = Long.MIN_VALUE;
  58. int maxIndex = -1;
  59. for(int i = 0;i < n;++i){
  60. long cur = leftSum[i]+rightSum[i]-nums[i]; // 区间和[0,i]+区间和[i,n-1]-nums[i]
  61. if(cur > res){
  62. res = cur;
  63. maxIndex = i;
  64. }
  65. }
  66. for(int i = maxIndex-1;i >= 0;--i){
  67. nums[i] = Math.min(nums[i],nums[i+1]);
  68. }
  69. for(int i = maxIndex+1;i < n;++i){
  70. nums[i] = Math.min(nums[i],nums[i-1]);
  71. }
  72. for(int i = 0;i < n;++i){
  73. System.out.printf("%d ",nums[i]);
  74. }
  75. }
  76. }

背包模型

菜药

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 基本思路:0-1背包问题
  5. */
  6. public static void main(String[] args){
  7. Scanner sc = new Scanner(System.in);
  8. int T = sc.nextInt(),M = sc.nextInt();
  9. int[][] arr = new int[M][2];
  10. for(int i = 0;i < M;++i){
  11. arr[i][0] = sc.nextInt(); arr[i][1] = sc.nextInt();
  12. }
  13. int[] dp = new int[T+1];
  14. for(int i = 0;i < M;++i){
  15. for(int j = T;j >= arr[i][0];--j){
  16. dp[j] = Math.max(dp[j],arr[i][1] + dp[j-arr[i][0]]);
  17. }
  18. }
  19. System.out.println(dp[T]);
  20. }
  21. }

装箱问题

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 基本思路:0-1背包问题
  5. */
  6. public static void main(String[] args){
  7. Scanner sc = new Scanner(System.in);
  8. int V = sc.nextInt(),n = sc.nextInt();
  9. int[] arr = new int[n];
  10. for(int i = 0;i < n;++i) arr[i] = sc.nextInt();
  11. int[] dp = new int[V+1];
  12. for(int i = 0;i < n;++i){
  13. for(int j = V;j >= arr[i];--j){
  14. dp[j] = Math.max(dp[j],arr[i] + dp[j-arr[i]]);
  15. }
  16. }
  17. System.out.println(V - dp[V]);
  18. }
  19. }

宠物小精灵之收服*

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 基本思路:0-1背包问题 + 回溯获取真实值
  5. 实际上也可以另外开一个二维数组同步记录伤害值,比较费空间
  6. */
  7. public static void main(String[] args){
  8. Scanner sc = new Scanner(System.in);
  9. int N = sc.nextInt(),M = sc.nextInt(),K = sc.nextInt();
  10. int[] nums = new int[K],damage = new int[K];
  11. for(int i = 0;i < K;++i){
  12. nums[i] = sc.nextInt(); damage[i] = sc.nextInt();
  13. }
  14. int[][] dp = new int[N+1][M];
  15. for(int i = 0;i < K;++i){
  16. for(int j = N;j >= nums[i];--j){
  17. for(int k = M-1;k >= damage[i];--k)// 这里注意体力不能为0
  18. dp[j][k] = Math.max(dp[j][k],1 + dp[j-nums[i]][k-damage[i]]);
  19. }
  20. }
  21. /*通过回溯获取收服最多小精灵都同时的最小伤害*/
  22. int t = M-1;
  23. while(t > 0 && dp[N][t] == dp[N][t-1]) --t;
  24. System.out.printf("%d %d\n",dp[N][M-1],M-t);
  25. }
  26. }

数字组合

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 基本思路:和为M的方案数目转换为0-1背包
  5. */
  6. public static void main(String[] args){
  7. Scanner sc = new Scanner(System.in);
  8. int N = sc.nextInt(),M = sc.nextInt();
  9. int[] dp = new int[M+1];
  10. dp[0] = 1;
  11. for(int i = 0;i < N;++i){
  12. int v = sc.nextInt();
  13. for(int j = M;j >= v;--j){
  14. dp[j] = dp[j] + dp[j-v];
  15. }
  16. }
  17. System.out.println(dp[M]);
  18. }
  19. }

买书

  1. import java.util.*;
  2. class Main{
  3. /*
  4. dp[i][j]: 前i个物品,价格恰好为j的方案数目
  5. 按照选取的第i个物品的个进行集合划分:
  6. 1)不选i dp[i-1][j]
  7. 2)选1个i,dp[i-1][j-w_i]
  8. ...
  9. 3 选n个i,dp[i-1][j- n*w_i]
  10. 1) dp[i][j] = dp[i-1][j] + dp[i-1][j-w] + dp[i-1][j-2w] + ... + dp[i-1][j-nw], j-nw >= 0
  11. 2) dp[i][j-w] = dp[i-1][j-w] + dp[i-1][j-2w] + ... + dp[i-1][j-n*w], j-nw >= 0
  12. 联立1),2)得:
  13. dp[i][j] = dp[i-1][j] + dp[i][j-w]
  14. */
  15. public static void main(String[] args){
  16. Scanner sc = new Scanner(System.in);
  17. int n = sc.nextInt();
  18. int[] dp = new int[n+1];
  19. dp[0] = 1;
  20. int[] w = {10,20,50,100};
  21. for(int i = 0;i < 4;++i){
  22. for(int j = w[i];j <= n;++j){
  23. dp[j] = dp[j] + dp[j-w[i]];
  24. }
  25. }
  26. System.out.println(dp[n]);
  27. }
  28. }

货币系统

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 同上
  5. */
  6. public static void main(String[] args){
  7. Scanner sc = new Scanner(System.in);
  8. int n = sc.nextInt(),m = sc.nextInt();
  9. long[] dp = new long[m+1];
  10. dp[0] = 1;
  11. for(int i = 0;i < n;++i){
  12. int w = sc.nextInt();
  13. for(int j = w;j <= m;++j){
  14. dp[j] = dp[j] + dp[j-w];
  15. }
  16. }
  17. System.out.println(dp[m]);
  18. }
  19. }

简化货币系统

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 思路:最优解是原有货币系统的子集
  5. 如何判断该元素是否多余?
  6. 当元素ai能够被小于它的元素集合线性表示出时,那么该元素就是在货币系统中是多余的,。
  7. 解题思路:排序+完全背包进行判断
  8. */
  9. public static void main(String[] args){
  10. Scanner sc = new Scanner(System.in);
  11. int T = sc.nextInt();
  12. while(T-- > 0){
  13. int n = sc.nextInt();
  14. int[] arr = new int[n];
  15. for(int i = 0;i < n;++i) arr[i] = sc.nextInt();
  16. Arrays.sort(arr);
  17. int mv = arr[n-1];
  18. int ans = 0;
  19. boolean[] dp = new boolean[mv+1]; // dp[i]表示面额i能够被之前的元素给凑出
  20. dp[0] = true;
  21. for(int i = 1;i <= n;++i){
  22. if(dp[arr[i-1]]) continue; // 已经被之前组合出则该面值是多余的
  23. ++ans; // 不能被面额小的元素组合出,必须有一张该面额的元素
  24. for(int j = arr[i-1];j <= mv;++j){
  25. dp[j] = dp[j] | dp[j-arr[i-1]];
  26. }
  27. }
  28. System.out.println(ans);
  29. }
  30. }
  31. }

多重背包

多重背包问题 基本思路 时间复杂度
朴素解决 转化为0-1背包 物品总的数目 * 背包容量
二进制拆分优化 对每种物品进行二进制拆分,降低物品总数 降低后的物品总数*背包容量
单调队列优化 状态计算时对背包容量进行分类,采用单调队列优化 物品的种类数目 * 背包容量
  1. 多重背包特点:与0-1背包和完全背包不同,物品的个数是有限个的
  2. 问题解决思路:
  3. 转换为0-1背包问题,总的时间复杂度:物品总数 * 背包容量
  4. 优化思路:对物品进行二进制拆分,降低每种物品的物品数目

庆功会

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 多重背包朴素解法:采用0-1背包的方式枚举所有物品
  5. 优化策略1:二进制优化转换为0-1背包
  6. 优化策略2:单调队列优化
  7. 朴素的时间复杂度:物品的总的数目 * 背包容量
  8. */
  9. public static void main(String[] args){
  10. Scanner sc = new Scanner(System.in);
  11. int N = sc.nextInt(),V = sc.nextInt();
  12. int[] nums = new int[N+1],v = new int[N+1],w = new int[N+1];
  13. for(int i = 1;i <= N;++i){
  14. v[i] = sc.nextInt(); w[i] = sc.nextInt(); nums[i] = sc.nextInt();
  15. }
  16. int[] dp = new int[V+1];
  17. for(int i = 1;i <= N;++i){
  18. for(int j = 0;j < nums[i];++j){
  19. for(int k = V; k >= v[i];--k){
  20. dp[k] = Math.max(dp[k],dp[k-v[i]] + w[i]);
  21. }
  22. }
  23. }
  24. System.out.println(dp[V]);
  25. }
  26. }

多重背包问题 III

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 多重背包的单调队列优化
  5. dp[i][j]:表示考虑前i个物品,背包容量为j的最大价值。
  6. 单调队列作用:用于快速获取上一层状态集合的最大值
  7. 优化思路:见官方题解
  8. 时间复杂度: 物品种类*背包容量
  9. 空间优化:可以采用滚动数组优化
  10. */
  11. public static void main(String[] args){
  12. Scanner sc = new Scanner(System.in);
  13. int N = sc.nextInt(),V = sc.nextInt();
  14. int[] nums = new int[N+1],v = new int[N+1],w = new int[N+1];
  15. for(int i = 1;i <= N;++i){
  16. v[i] = sc.nextInt(); w[i] = sc.nextInt(); nums[i] = sc.nextInt();
  17. }
  18. int[][] dp = new int[N+1][V+1];
  19. int[] q = new int[20100]; // 单调递减队列中维护的是体积
  20. for(int i = 1;i <= N;++i){
  21. for(int r = 0;r < v[i];++r){ // 对容量进行分类(两层循环本质上对容量进行枚举)
  22. int head = 0,tail = -1; // 清空单调队列
  23. for(int j = r;j <= V;j += v[i]){
  24. while(head <= tail && j-q[head] > nums[i] * v[i]) ++head; // 维护窗口大小,(j-q[tail])/v[i]*w[i]表示选择当前的物品个数*价值
  25. while(head <= tail && dp[i-1][q[tail]] + (j-q[tail])/v[i]*w[i] <= dp[i-1][j]) --tail; // 维护窗口最大值
  26. q[++tail] = j;
  27. dp[i][j] = dp[i-1][q[head]] + (j-q[head])/v[i]*w[i];
  28. }
  29. }
  30. }
  31. System.out.println(dp[N][V]);
  32. }
  33. }

二维费用的背包问题

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 0-1背包思路,状态表示时多了一个维度
  5. */
  6. public static void main(String[] args){
  7. Scanner sc = new Scanner(System.in);
  8. int N = sc.nextInt(), V = sc.nextInt(),M = sc.nextInt();
  9. int[][] dp = new int[V+1][M+1];
  10. for(int i = 1;i <= N;++i){
  11. int v = sc.nextInt(),m = sc.nextInt(),w = sc.nextInt();
  12. for(int j = V;j >= v;--j){
  13. for(int k = M;k >= m;--k){
  14. dp[j][k] = Math.max(dp[j][k],dp[j-v][k-m]+w);
  15. }
  16. }
  17. }
  18. System.out.println(dp[V][M]);
  19. }
  20. }

1020. 潜水员

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 基本思路:多维0-1背包问题
  5. 注意本题对于两个维度“体积”的限制:体积至少是
  6. 思维拓展:"体积"不超过或者"体积恰好"的初始化和状态转移
  7. */
  8. public static void main(String[] args){
  9. Scanner sc = new Scanner(System.in);
  10. int m = sc.nextInt(),n = sc.nextInt();
  11. int k = sc.nextInt();
  12. // dp[j][k]表示氧气至少为j,氮气至少为k的最小重量
  13. int[][] dp = new int[m+1][n+1];
  14. for(int i = 0;i <= m;++i) Arrays.fill(dp[i],Integer.MAX_VALUE); // 表示不可达状态
  15. dp[0][0] = 0;
  16. for(int i = 1;i <= k;++i){
  17. int a = sc.nextInt(),b = sc.nextInt(),c = sc.nextInt();
  18. for(int j = m;j >= 0;--j){
  19. for(int t = n;t >= 0;--t){
  20. int jj = Math.max(0,j-a),tt = Math.max(0,t-b); // 注意这里的负数也是合法状态等价于0
  21. if(dp[jj][tt] != Integer.MAX_VALUE)
  22. dp[j][t] = Math.min(dp[j][t],dp[jj][tt] + c);
  23. }
  24. }
  25. }
  26. System.out.println(dp[m][n]);
  27. }
  28. }

状态机模型

预备知识

更多关于状态机参考文章有限状态机的理解

有限状态机定义(FSM):有一系列的状态(state)。根据输入的不同,FSM 从一个状态切换到另一个状态。在这些状态中,有一些状态是特殊的状态 —— 接受状态(accept state)。如果输入处理完毕,FSM 停留在接受状态,那么 FSM 处理成功,否则处理失败

实例1

  1. 写一个状态机,验证一串二进制bit,包含偶数个0和奇数个1

状态定义

状态1 状态2 状态3 状态4
定义 偶数个 0 和偶数个 1 偶数个 0 和奇数个 1 奇数个 0 和偶数个 1 奇数个 0 和奇数个 1
符号表示 EE EO OE OO

初始状态:空二进制串即0个0,0个1即EE

触发状态切换的动作:输入一个字符

状态机模型

1 动态规划与搜索 - 图1

状态转换:图中EE状态的字符,新增1个1则变为EO

接受状态:EO(即所有输入处理完后是接受状态即偶数个0奇数个1,则FSM处理成功,否则失败)

注意:不要把状态与状态切换的动作搞混

解题步骤

  1. step1:定义所有可能状态
  2. step2:对于当前时刻的每个状态,判断其能够由上个时刻的状态转移过来,写出状态转移方程
  3. step3:初始化状态,并迭代推出最终结果。

股票买卖 V

  1. import java.util.*;
  2. class Main{
  3. /*
  4. 状态机设计
  5. 0 不持有股票非冷冻期
  6. 1 持有股票
  7. 2 不持有股票的冷冻期
  8. 0 -> 0
  9. 0 -> 1
  10. 1 -> 2
  11. 1 -> 1
  12. 2 -> 0
  13. dp[i][0] = max(dp[i-1][0],dp[i-1][2])
  14. dp[i][1] = max(dp[i-1][0]-w,dp[i-1][1])
  15. dp[i][2] = dp[i-1][1]+w
  16. }
  17. */
  18. public static void main(String[] args){
  19. Scanner sc = new Scanner(System.in);
  20. int n = sc.nextInt();
  21. int[] nums = new int[n];
  22. for(int i = 0;i < n;++i){
  23. nums[i] = sc.nextInt();
  24. }
  25. int[][] dp = new int[n+1][3];
  26. dp[0][0] = 0;
  27. dp[0][1] = -100000;
  28. dp[0][2] = -100000;
  29. for(int i = 1;i <= n;++i){
  30. dp[i][0] = Math.max(dp[i-1][0],dp[i-1][2]);
  31. dp[i][1] = Math.max(dp[i-1][0]-nums[i-1],dp[i-1][1]);
  32. dp[i][2] = dp[i-1][1] + nums[i-1];
  33. }
  34. int res = Math.max(dp[n][0],Math.max(dp[n][1],dp[n][2])); // 注意三个状态都有可能
  35. System.out.println(res);
  36. }
  37. }

股票买卖 IV

  1. import java.util.*;
  2. import java.io.*;
  3. class Main{
  4. /*
  5. 0 1
  6. 不持有 持有
  7. 0 -> 1 dp[i][k][1] = dp[i-1][k][0]-w
  8. 0 -> 0 dp[i][k][0] = dp[i-1][k][0]
  9. 1 -> 0 dp[i][k][0] = dp[i-1][k-1][1]+w
  10. 1 -> 1 dp[i][k][1] = dp[i-1][k][1]
  11. dp[i][k][]到第i天位置,不超过k次交易,不持有/持有股票的最大值
  12. */
  13. public static void main(String[] args) throws NumberFormatException, IOException{
  14. BufferedReader buf=new BufferedReader(new InputStreamReader(System.in));
  15. String[] strs = buf.readLine().split(" ");
  16. int N = Integer.parseInt(strs[0]);
  17. int k = Integer.parseInt(strs[1]);
  18. String[] nstrs = buf.readLine().split(" ");
  19. int[] nums = new int[N];
  20. for(int i = 0;i < N;++i){
  21. nums[i] = Integer.parseInt(nstrs[i]);
  22. }
  23. int[][][] dp = new int[N+1][k+1][2];
  24. for(int j = 0;j <= k;++j){ // 不可达状态
  25. dp[0][j][1] = -0x3f3f3f3f;
  26. }
  27. dp[0][0][0] = 0;
  28. for(int i = 1;i <= N;++i){
  29. for(int j = 0;j <= k;++j){
  30. dp[i][j][1] = Math.max(dp[i-1][j][1],dp[i-1][j][0]-nums[i-1]);
  31. dp[i][j][0] = dp[i-1][j][0];
  32. if(j >= 1)
  33. dp[i][j][0] = Math.max(dp[i][j][0],dp[i-1][j-1][1]+nums[i-1]);
  34. }
  35. }
  36. int res = Math.max(dp[N][k][0],dp[N][k][1]);
  37. System.out.println(res);
  38. buf.close();
  39. }
  40. }
  1. 定义了m+1个状态
  2. f[i][j]表示已经读取i个字符且到达KMP的第j个状态的字符串数目
  3. 任意一个不包含T的子串等价于走不到状态m
  4. 每输入一个字符可以对应K
  5. f[i][j]可不可以这么理解:i是状态机上走的步数,j是具体走到哪里。对于当前的位置j,每次走一步的结果,要么走到下一个位置(即原串和匹配串相等)要么回退(返回到0或原串和字符串相等的位置)

状压DP

  1. 状态压缩DP分成两大类:
  2. 1)棋盘式DP
  3. 2)集合式DP

棋盘式

小国王

  1. n×n 的棋盘上放 k 个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。
  2. 基本思路:
  3. 状态表示: f[i][j][s]表示第i行已经放了j个棋子,最后一行的状态为s的方案数
  4. step1:确定集合以及集合的属性
  5. 集合(方案数进行分类):所有只摆在前i行,已经摆了j个国王,且第i行摆放的状态是s的所有方案数目
  6. 属性:方案数目
  7. step2:状态计算
  8. 如何枚举每一层的状态?
  9. 要求:
  10. 1)第i-1行与第i行之间也不能相互攻击到。
  11. 2)第i行内部不能有两个相邻1
  12. ==================================
  13. 对于要求1)可以转换为位运算(用整型变量a,b表示第i-1行和i行的状态):
  14. 条件1)等价于 (a&b) == 0 并且(a || b)不能有连续的1
  15. 已经摆了前i行,使用了j个国王,并且第i行的状态是a = 已经摆了前i-1行,使用了j-count(a)个国王,并且第i-1排状态是b的方案数目
  16. dp[i][j][a] = dp[i-1][j-count(a)][b]
  17. 时间复杂度:状态数目*状态计算的时间

编码实现步骤

  1. step1:保存所有合法状态
  2. step2:获取所有合法状态中每个状态可转移的状态集合
  3. step3:状态转移求出方案数目

实现

  1. import java.util.*;
  2. class Main{
  3. /*判断变量的二进制中是否有相邻的1,没有返回true
  4. 如果存在相邻,则是非法状态,返回false,否则返回true
  5. */
  6. static boolean check(int s){
  7. return (s&(s >> 1)) == 0;
  8. }
  9. public static void main(String[] args){
  10. Scanner sc = new Scanner(System.in);
  11. int n = sc.nextInt();
  12. int k = sc.nextInt();
  13. /*每一行国王摆放不同的状态数目最大值,棋盘的最大长度为10,
  14. 每个格子可以放国外也可以不放则最多方案数目为 2^10, 这里设置为 2^10+20留有余量
  15. 显然2^10摆放方案中有很多不合法的摆放
  16. */
  17. int stateNum = (1 << 10)+20;
  18. long[][][] dp = new long[12][110][stateNum];
  19. int[] cnt = new int[stateNum]; // 每个状态对应的二进制1的个数即国王的个数
  20. //step1:二进制枚举单层所有的摆放方案,并保存合法的摆放方案
  21. ArrayList<Integer> state = new ArrayList<>();
  22. for(int s = 0;s < (1 << n);++s){ // 二进制枚举状态范围为[0,2^n-1]
  23. if(check(s)){ // 国王摆放是否合法,对于单行数据只要确保两个国外不相邻即可,即
  24. state.add(s);
  25. cnt[s] = Integer.bitCount(s); // 存储当前合法状态所用棋子个数
  26. }
  27. }
  28. /*
  29. step2:选出每个状态能够存储的合法状态并存储,
  30. head[s1]中存储的是s1能够转移到的状态集合
  31. 即s1和集合中的任意状态是棋盘上合法的相邻状态
  32. 假设状态a,b,a与b从行来看都是合法状态,之前已经保存了所有状态
  33. a与b要如何是合法的相邻状态?
  34. 满足:a&b == 0 并且 a || b 不存在相邻的1
  35. */
  36. //用于保存每个状态对应的合法转移状态集合
  37. ArrayList<Integer>[] heads = new ArrayList[stateNum];
  38. for(int i = 0;i < stateNum;++i) heads[i] = new ArrayList<>();
  39. for(int i = 0;i < state.size();++i){
  40. for(int j = 0;j < state.size();++j){
  41. int s1 = state.get(i);
  42. int s2 = state.get(j);
  43. if((s1&s2) == 0 && check(s1|s2)){
  44. heads[i].add(j); // 存储坐标,不直接存储状态值
  45. }
  46. }
  47. }
  48. /*
  49. step3:进行状态转移统计统计方案住
  50. dp[i][j][s]:使用j个国王摆了i行,最后一行状态为s的方案数目
  51. 时间复杂度估算: 行数*棋子个数*合法状态数目*状态计算代价
  52. < 10*100*合法状态数目*每个状态可转移的状态个数 约等于 1.365 x 10^6
  53. 合法状态并没有想象那么多。
  54. */
  55. dp[0][0][0] = 1; /*初始化摆放0行各个状态*/
  56. for(int i = 1;i <= n+1;++i){
  57. for(int j = 0;j <= k;++j){
  58. /*枚举第i行的合法状态,对于每个状态,i-1行有多个合法的状态可以转移过来*/
  59. for(int a = 0;a < state.size();++a){
  60. for(int b = 0;b < heads[a].size();++b){
  61. /*状态转移:i行的s1 <- i-1行状态集合(s2)*/
  62. int s1 = state.get(a);
  63. int s2 = heads[a].get(b);
  64. if(j >= cnt[s1]){ // 当前国王数目够用才能转移
  65. dp[i][j][a] += dp[i-1][j-cnt[s1]][s2];
  66. }
  67. }
  68. }
  69. }
  70. }
  71. System.out.println(dp[n+1][k][0]); // 摆放n+1行国王用了k个棋子最后一行不摆放棋子就是答案
  72. }
  73. }

327 玉米田

数位DP

问题特点

  1. 题目特点:求区间[x,y]满足特定性质的元素个数
  2. -------------------------------------------------------------------------
  3. 解决思路的关键点:
  4. 技巧1 [x,y] => f(y)-f(x-1) (前缀和)
  5. 问题转换为: 区间[1,y]的满足性质的元素个数-区间[1,x-1]满足性质的元素个数
  6. ------------------------------------------------------------------------
  7. 技巧2:利用树的结构来考虑(按位分类讨论)

模型分析

1 动态规划与搜索 - 图2

  1. 每个位置的数字的值都是有限定范围的,记为a,可选的值x在[0,a]范围内,因此可以分为两种情况讨论:
  2. 1x = a
  3. 2) x < a

度的数量

  1. 问题:求给定区间 [X,Y] 中满足下列条件的整数个数:这个数恰好等于 K 个互不相等的 B 的整数次幂之和。
  2. 实例:X=15,Y=20,K=2,B=2 ,则有且仅有下列三个数满足题意:
  3. 17=2^4+2^0
  4. 18=2^4+2^1
  5. 20=2^4+2^2
  1. import java.util.*;
  2. /*
  3. 题目要求: 给定B进制数的范围,请该范围内的数目满足以下要求的数字个数:
  4. 1)由0和1组成
  5. 2)1的个数恰好为K个
  6. 记[X-1,Y]的B进制范围表示为[x,y]
  7. 原问题等价于求 [1,y]范围内满足要求的数 - [1,x]范围内满足要求的数
  8. */
  9. class Main{
  10. public static void main(String[] args){
  11. Scanner sc = new Scanner(System.in);
  12. int X = sc.nextInt(),Y = sc.nextInt();
  13. int K = sc.nextInt(),B = sc.nextInt();
  14. // step1:组合计算结果打表
  15. int[][] comb = new int[33][33]; // 各个进制下数据的长度不会超过32位
  16. for(int i = 0;i < 33;++i){
  17. for(int j = 0;j <= i;++j){
  18. comb[i][j] = j == 0 ? 1 : comb[i-1][j] + comb[i-1][j-1];
  19. }
  20. }
  21. // step2:将X,Y转换为B进制数组,低位在前
  22. int[] nums1 = convert(X-1,B);
  23. int[] nums2 = convert(Y,B);
  24. int res = count(nums2,K,comb) - count(nums1,K,comb);
  25. System.out.println(res);
  26. }
  27. static int[] convert(int v,int B){
  28. List<Integer> list = new ArrayList<>();
  29. while(v > 0){
  30. list.add(v%B); v = v/B;
  31. }
  32. int[] res = list.stream().mapToInt(Integer::valueOf).toArray();
  33. return res;
  34. }
  35. /*
  36. 求[1,x]范围内在满足题目要求的数字个数(从树的角度进行讨论)
  37. 对于B进制数arr(共n位),从最高位(第n位)开始遍历每1位,对于第i位,cur表示已经取到的1的个数。
  38. 第i位上的数字x有三种情况:
  39. 1;当前位为大于1时:
  40. 这位可以取0,那么满足条件的方案数为就是剩下位中取k-cur个1;
  41. 这位可以取1, 那么满足条件的方案数为就是剩下位中取k-cur-1个1
  42. 2:当这位是1时,
  43. 方案数目等于为0的方案数目(剩下位中取k-cur个1)
  44. + 设置为1后(++cur),后续循环的方案数目
  45. 3:当这位是0时, 这位只能取0, 那么只能看下一位的,循环继续
  46. */
  47. static int count(int[] arr,int K,int[][] comb){
  48. int len = arr.length;
  49. int ans = 0,cur = 0; // 方案数目,1的个数
  50. for(int i = len-1;i >= 0;--i){ // 从高位开始遍历
  51. if(arr[i] > 1){ // 情况1
  52. if(K-cur >= 0) ans += comb[i][K-cur]; // 取0
  53. // 当前位大于1,并设置为1的时候,方案数目等于 从剩余i位中选择K-1-cur个数
  54. if(K-1-cur >= 0) ans += comb[i][K-1-cur]; // 取1
  55. break; // !!!!!!!!!!
  56. }else if(arr[i] == 1){ // 情况2
  57. if(K-cur >= 0) ans += comb[i][K-cur]; // 取0
  58. ++cur; // 取1
  59. if(cur > K) break; // 后续都不满足
  60. }
  61. if(i == 0 && cur == K) ++ans; // 特殊情况
  62. }
  63. return ans;
  64. }
  65. }

树形DP

  • 注意这里的树并非特指二叉树,而是更加广义的树,满足n个节点并且节点之间仅仅存在1条边,即n-1条边

力扣的树形DP题目(二叉树)_

543. 二叉树的直径

124. 二叉树中的最大路径和

687. 最长同值路径

多叉树的树形DP

1072 树的最长路径

此处最长路径的定义:路径中边的数量最多

基本思路

  1. 树的直径思路:
  2. step1:任取一点作为起点,找到距离该点最远的一个点u。(DFS,BFS
  3. step2:再找到距离u最远的一点v,DFS,BFS
  4. u,v之间的路径就是一条直径。
  5. ==========================================================================
  6. 按照类别枚举所有直径。
  7. 分类依据:每个路径都有一个最高点,如果两个路径的最高点相同,那么这两条路径就是同类路径。
  8. 基本步骤:
  9. step1:枚举树中每个点。
  10. step2:找到这个点代表的一类路径的最大值。
  11. 问题:如何找到这个点代表的路径最大值?
  12. 基本思路:找到从该节点出发向下的所有路径的最大值与次大值即
  13. 该点代表的一类路径最大值 = 从该点出发的所有向下路径的最大值与次大值
  14. 如何保证搜索的路径时向下的?
  15. 策略1:将访问当前节点之间的一个节点(父节点)传入,搜索时不重复搜索当前节点的父节点
  16. 原问题:多叉树种的最长路径
  17. 原问题拆解为子问题:包含当前节点且当前节点是路径的最高处的所有路径最大值
  18. 子问题:
  19. 1)保存从当前节点出发的所有向下路径的最大值与次大值,从而计算子节点为路径最高点的所有的路径提供的最大贡献(或者说子节点为根节点的路径最大值)
  20. 2)提供当前节点为其父亲节点为根节点的树提供的最大贡献。
  21. 注意:这里最大贡献的值 不是 子问题的值 而是 树的多侧所能提供的最大路径 !!!!!!

核心思想子节点更新父节点信息

实现

  1. import java.util.*;
  2. class Main{
  3. static int[] element; // 链表节点值
  4. static int[] np; // 单链表相邻节点地址
  5. static int[] heads; // 单链表头
  6. static int[] w; // 链表节点边值
  7. static int idx = 0;
  8. public static void main(String[] args){
  9. Scanner sc = new Scanner(System.in);
  10. int n = sc.nextInt();
  11. int N = n*2+10;
  12. element = new int[N];
  13. np = new int[N];
  14. heads = new int[N];
  15. w = new int[N];
  16. Arrays.fill(heads,-1); // dummy node指向为空
  17. for(int i = 0;i < n-1;++i){
  18. int a = sc.nextInt();
  19. int b = sc.nextInt();
  20. int v = sc.nextInt();
  21. add(a,b,v); add(b,a,v);
  22. }
  23. dfs(1,-1);
  24. System.out.println(res);
  25. }
  26. /*head[a]->b->-1(头插法)*/
  27. static void add(int a,int b,int v){
  28. element[idx] = b;
  29. w[idx] = v;
  30. np[idx] = heads[a];
  31. heads[a] = idx;
  32. ++idx;
  33. }
  34. /*返回从src出发向下最长路径和,把src节点提起来*/
  35. static int res = 0;
  36. static int dfs(int src,int father){
  37. int sum = 0;
  38. int subMax1 = 0,subMax2 = 0; // 最大和次大路径和
  39. for(int next = heads[src];next != -1;next = np[next]){
  40. int node = element[next];
  41. int weight = w[next];
  42. if(node == father)
  43. continue;
  44. int dis = dfs(node,src) + weight;
  45. if(dis > subMax1) {
  46. subMax2 = subMax1;
  47. subMax1 = dis;
  48. }else if(dis > subMax2){
  49. subMax2 = dis;
  50. }
  51. }
  52. res = Math.max(res,subMax1+subMax2);
  53. // System.out.println(subMax1);
  54. return subMax1;
  55. }
  56. }

3760 最大剩余油量

基本思路

  1. 直观想法就是枚举树中所有路径,枚举方式,分类枚举。
  2. 分类依据:每个路径都有一个最高点,如果两个路径的最高点相同,那么这两条路径就是同类路径。
  3. 该点代表的一类路径最大值 = 从该点出发的所有向下路径的最大值与次大值 包含该点的油量最大值 = 从该点出发的所有向下路径的油量的最大值与次大值
  4. 注意:此题解法上类似于1072,这种枚举方式能够确保考虑到所有路径情况。

实现

  1. import java.util.*;
  2. class Solution{
  3. int[] ele; // 节点编号存储
  4. int[] val; // 链表节点的节点值存储
  5. int[] wgh; // 链表节点的边的权重存储
  6. int[] np; // next point
  7. int[] hds; // 链表节点头指针维护
  8. int idx;
  9. Solution(){}
  10. void solve(){
  11. Scanner sc = new Scanner(System.in);
  12. int n = sc.nextInt();
  13. int N = n*2;
  14. val = new int[n+1]; // id->节点值
  15. hds = new int[n+1]; // 邻接表
  16. ele = new int[N];
  17. wgh = new int[N];
  18. np = new int[N];
  19. idx = 0;
  20. Arrays.fill(hds,-1);
  21. for(int i = 1;i <= n;++i){
  22. val[i] = sc.nextInt();
  23. }
  24. /*建立多叉树*/
  25. for(int i = 0;i < n-1;++i){
  26. int a = sc.nextInt();
  27. int b = sc.nextInt();
  28. int w = sc.nextInt();
  29. add(a,b,w); add(b,a,w);
  30. }
  31. downDFS(1,-1);
  32. System.out.println(res);
  33. }
  34. // 分配一个节点并头插法插入到a节点的链表中
  35. void add(int a,int b,int w){
  36. ele[idx] = b;
  37. wgh[idx] = w;
  38. np[idx] = hds[a];
  39. hds[a] = idx;
  40. ++idx;
  41. }
  42. // 自顶向下枚举每个节点,当前节点的最大值 = 子节点的最大值+子节点的次大值+当前节点值
  43. long res = 0;
  44. long downDFS(int src,int father){
  45. long maxv1 = 0,maxv2 = 0;
  46. // 遍历该节点的邻接链表
  47. for(int next = hds[src];next != -1;next = np[next]){
  48. int nodeId = ele[next];
  49. int edgeWeight = wgh[next];
  50. if(nodeId == father) continue;
  51. long sum = downDFS(nodeId,src)-edgeWeight;
  52. if(sum < 0) continue; // 油量贡献 < 0,跳过
  53. if(sum > maxv1){
  54. maxv2 = maxv1; maxv1 = sum;
  55. }else if(sum > maxv2){
  56. maxv2 = sum;
  57. }
  58. }
  59. /*包含当前节点的所有路径的最大剩余油量 = 左贡献最大油量+右最大贡献油量+节点值*/
  60. res = Math.max(maxv1+maxv2+val[src],res);
  61. /*返回包含当前城市的所有向下路径的剩余的最大油量*/
  62. return maxv1+val[src];
  63. }
  64. }
  65. class Main{
  66. public static void main(String[] args){
  67. new Solution().solve();
  68. }
  69. }

1073 树的中心

基本思路

1 动态规划与搜索 - 图3

树的中心:距离该节点的最远节点的路径的边是所有节点中最少的。

  1. step1(子节点更新父节点信息):通过一次dfs,我们可以获得每个节点的向下路径的最大值与次大值以及最大值路径的节点。
  2. 对于图中的每个节点存储三类信息:
  3. 1)当前节点出发向下路径的最大值
  4. 2)当前节点出发向下路径的次大值
  5. 3)当前节点出发向下路径的最大值经过的邻居节点。
  6. step2(父节点信息更新子节点信息):二次dfs,注意此次dfs搜索的顺序与第一次dfs要完全一致,我们需要利用之间存储的三类信心求向上路径的最大值。
  7. 当前节点的最大值 = max(父节点向上路径最大值,父节点向下路径(最大||次大)值) + 当前节点到父节点的边的权重
  8. 其中父节点向上路径最大值在遍历的过程中进行记录。
  9. ============================================================================================
  10. 情况1:父节点向下路径最大值不经过当前节点:
  11. 当前节点的最大值 = max{父节点向下路径的最大值,父节点向上路径最大值} + 当前节点到父节点的边的权重
  12. 情况2:父节点向下路径的最大值经过当前节点(如果使用最大值就是重复计算父节点与当前节点边的权重)
  13. 当前节点的最大值 = {父节点向下路径的次大值,父节点向上路径最大值} + 当前节点到父节点的边的权重
  14. ======================================================================================
  15. 注意:本题没有考虑负权重,如果有负权重则需要考虑边界情况

核心思想子节点更新父节点信息存储,父节点更新子节点信息综合使用。

  1. import java.util.*;
  2. class Main{
  3. static int[] ele; // element
  4. static int[] np; // next pointer
  5. static int[] wht; // weight(注意题目中没有负权值,有负权值需要根据题意调整)
  6. static int[] hds; // heads
  7. static int idx = 0;
  8. static int INF = 0;
  9. public static void main(String[] args){
  10. Scanner sc = new Scanner(System.in);
  11. int n = sc.nextInt();
  12. int N = n*2+10;
  13. ele = new int[N];
  14. np = new int[N];
  15. wht = new int[N];
  16. hds = new int[n+1];
  17. Arrays.fill(hds,-1);
  18. for(int i = 0;i < n-1;++i){
  19. int a = sc.nextInt();
  20. int b = sc.nextInt();
  21. int w = sc.nextInt();
  22. add(a,b,w); add(b,a,w);
  23. }
  24. downMax1 = new int[n+1];
  25. downMax2 = new int[n+1];
  26. upMax = new int[n+1];
  27. neighbour = new int[n+1];
  28. Arrays.fill(downMax1,INF);
  29. Arrays.fill(downMax2,INF);
  30. Arrays.fill(upMax,INF);
  31. dfs_down(1,-1);
  32. dfs_up(1,-1);
  33. int res = Integer.MAX_VALUE;
  34. for(int i = 1;i <= n;++i){
  35. int val = Math.max(downMax1[i],upMax[i]); // 以当前节点为出发点到其他节点的最大值
  36. // System.out.printf("%d %d %d %d\n",i,downMax1[i],downMax2[i],upMax[i]);
  37. res = Math.min(res,val);
  38. }
  39. System.out.println(res);
  40. }
  41. static void add(int a,int b,int w){
  42. ele[idx] = b;
  43. wht[idx] = w;
  44. np[idx] = hds[a];
  45. hds[a] = idx;
  46. ++idx;
  47. }
  48. /*当前节点向下路径最大值,次大值,以及最大值经过的节点
  49. 显然对于叶节点没有向下路径
  50. */
  51. static int[] downMax1;
  52. static int[] downMax2;
  53. static int[] neighbour;
  54. static int dfs_down(int src,int father){
  55. int max1 = 0,max2 = 0;
  56. for(int next = hds[src];next != -1;next = np[next]){
  57. int node = ele[next];
  58. int w = wht[next];
  59. if(node == father) continue;
  60. int dis = dfs_down(node,src)+w;
  61. if(dis > max1){
  62. max2 = max1; max1 = dis;
  63. downMax1[src] = max1; neighbour[src] = node; downMax2[src] = max2;
  64. }else if(dis > max2){
  65. max2 = dis;
  66. downMax2[src] = max2;
  67. }
  68. }
  69. return max1;
  70. }
  71. /*当前节点向上路径的最大值,根据第一次dfs获取的信息求取
  72. 显然对于根节点(搜索起点)没有向上路径
  73. */
  74. static int[] upMax;
  75. static void dfs_up(int src,int father){
  76. for(int next = hds[src];next != -1;next = np[next]){
  77. int node = ele[next];
  78. int w = wht[next];
  79. if(node == father) continue;
  80. // 利用父节点去更新每个子节点的向上路径最大值
  81. if(neighbour[src] == node){
  82. upMax[node] = Math.max(downMax2[src],upMax[src])+w;
  83. }else{
  84. upMax[node] = Math.max(downMax1[src],upMax[src])+w;
  85. }
  86. dfs_up(node,src);
  87. }
  88. }
  89. }

1075 数字转换

a能被b整除,或b能整除a。a称为b的倍数,b称为a的约数

基本思想:树的建立

节点:数字,边:其中一个数字的约数之和等于另外一个数字

目标:树的最长路径,路径中节点的最大值不超过N。

  1. import java.util.*;
  2. class Tree{
  3. int[] e;
  4. int[] np;
  5. int[] h;
  6. int idx = 0;
  7. Tree(int N){
  8. e = new int[N];
  9. np = new int[N];
  10. h = new int[N];
  11. Arrays.fill(h,-1);
  12. }
  13. void add(int a,int b){
  14. e[idx] = b; np[idx] = h[a]; h[a] = idx; ++idx;
  15. }
  16. }
  17. class Main{
  18. static int res = 0;
  19. static int dfs(Tree tree,int src,int father){
  20. int max1 = 0,max2 = 0;
  21. for(int next = tree.h[src];next != -1;next = tree.np[next]){
  22. int id = tree.e[next];
  23. if(id == father) continue;
  24. int val = dfs(tree,id,src)+1;
  25. if(val > max1){
  26. max2 = max1;max1 = val;
  27. }else if(val > max2){
  28. max2 = val;
  29. }
  30. }
  31. res = Math.max(res,max1+max2); // 求取最长直径
  32. return max1;
  33. }
  34. public static void main(String[] args){
  35. Scanner sc = new Scanner(System.in);
  36. int n = sc.nextInt();
  37. Tree tree = new Tree(n*2); // 数据容量确保存储所有边即可
  38. /*step1:统计每个数组对应的约数之和
  39. 正向思维:对每个数据求其所有约数,然后相加求和 O(n* 根号n)
  40. 逆向思维: 对于每个数看他是哪些数的约数,进行累加求和 O(nlog级别)
  41. */
  42. int[] cnt = new int[n+1]; // 存储每个数的约数之和
  43. for(int i = 1;i <= n;++i){ // 枚举数i
  44. for(int j = 2;j <= n/i;++j){ // i是i*j的约数(这里需要注意j从2开始,因为1不是本身的约数)
  45. cnt[i*j] += i;
  46. }
  47. }
  48. /*
  49. step2:建立无向图
  50. 这里的无向图本质上是森林的集合
  51. */
  52. boolean[] isTreeNode = new boolean[n+1];
  53. for(int i = 1;i <= n;++i){
  54. if(cnt[i] > 0 && i > cnt[i]){
  55. tree.add(i,cnt[i]);
  56. tree.add(cnt[i],i);
  57. isTreeNode[i] = true; // 只需要标记一个即可,避免重复搜索
  58. }
  59. }
  60. /*
  61. 包含最大直径的树一定含有节点1,因此直接从1开始搜索,避免超时
  62. step3:求取树的最大直径
  63. */
  64. dfs(tree,1,-1);
  65. System.out.println(res);
  66. }
  67. }

搜索

Flood Fill思想

  1. 本质上是BFS/DFS搜索,注意: 1)越界问题 2)重复访问问题(BFS入队标记,不要采用出队标记) 即可。

池塘计数

  1. import java.util.*;
  2. class Main{
  3. static boolean[][] vis;
  4. static String[] map;
  5. public static void main(String[] args){
  6. Scanner sc = new Scanner(System.in);
  7. int N = sc.nextInt(),M = sc.nextInt();
  8. vis = new boolean[N][M]; map = new String[N];
  9. for(int i = 0;i < N;++i) map[i] = sc.next();
  10. int cnt = 0;
  11. for(int r = 0;r < N;++r){
  12. for(int c = 0;c < M;++c){
  13. if(bfs(r,c)) ++cnt;
  14. }
  15. }
  16. System.out.println(cnt);
  17. }
  18. /*通过BFS进行Flood Fill*/
  19. public static boolean bfs(int r,int c){
  20. if(vis[r][c] || map[r].charAt(c) != 'W') return false;
  21. int row = map.length,col = map[0].length();
  22. Queue<int[]> q = new ArrayDeque<>();
  23. q.offer(new int[]{r,c});
  24. vis[r][c] = true;
  25. while(!q.isEmpty()){
  26. int[] t = q.poll();
  27. for(int i = -1;i <= 1;++i){
  28. for(int j = -1;j <= 1;++j){
  29. int rr = t[0] + i,cc = t[1] + j;
  30. if(rr >= 0 && rr < row && cc >= 0 && cc < col && !vis[rr][cc] && map[rr].charAt(cc) == 'W'){
  31. q.offer(new int[]{rr,cc});
  32. vis[rr][cc] = true;
  33. }
  34. }
  35. }
  36. }
  37. return true;
  38. }
  39. }

城堡问题

  1. import java.util.*;
  2. class Main{
  3. static boolean[][] vis;
  4. static int[][] map;
  5. public static void main(String[] args){
  6. Scanner sc = new Scanner(System.in);
  7. int m = sc.nextInt(),n = sc.nextInt();
  8. vis = new boolean[m][n]; map = new int[m][n];
  9. for(int r = 0;r < m;++r){
  10. for(int c = 0;c < n;++c){
  11. map[r][c] = sc.nextInt();
  12. }
  13. }
  14. int cnt = 0;
  15. int area = 0;
  16. for(int r = 0;r < m;++r){
  17. for(int c = 0;c < n;++c){
  18. int tmp = bfs(r,c);
  19. if(tmp > 0){
  20. ++cnt;
  21. area = Math.max(tmp,area);
  22. }
  23. }
  24. }
  25. System.out.println(cnt);
  26. System.out.println(area);
  27. }
  28. /*通过BFS进行Flood Fill*/
  29. // 西 北 东 南
  30. static int[] dr = {0,-1,0,1};
  31. static int[] dc = {-1,0,1,0};
  32. public static int bfs(int r,int c){
  33. if(vis[r][c]) return 0;
  34. int row = map.length,col = map[0].length;
  35. Queue<int[]> q = new ArrayDeque<>();
  36. q.offer(new int[]{r,c});
  37. vis[r][c] = true;
  38. int area = 1;
  39. while(!q.isEmpty()){
  40. int[] t = q.poll();
  41. int rr = t[0],cc = t[1];
  42. int val = map[rr][cc];
  43. for(int i = 0;i < 4;++i){
  44. int nr = rr + dr[i],nc = cc + dc[i];
  45. if(nr >= 0 && nr < row && nc >= 0 && nc < col){
  46. if(((val >> i) & 1) != 1 && !vis[nr][nc]){ // 当前方向没有墙且没有fill
  47. q.offer(new int[]{nr,nc});
  48. vis[nr][nc] = true;
  49. ++area;
  50. }
  51. }
  52. }
  53. }
  54. return area;
  55. }
  56. }

山峰和山谷

  1. import java.util.*;
  2. class Main{
  3. static boolean[][] vis;
  4. static int[][] map;
  5. /*
  6. 基本思路:BFS的Flood Fill
  7. 注意:标志位的设计非常巧妙
  8. */
  9. public static void main(String[] args){
  10. Scanner sc = new Scanner(System.in);
  11. int n = sc.nextInt();
  12. vis = new boolean[n][n]; map = new int[n][n];
  13. for(int r = 0;r < n;++r){
  14. for(int c = 0;c < n;++c){
  15. map[r][c] = sc.nextInt();
  16. }
  17. }
  18. int peek = 0,bottom = 0;
  19. for(int r = 0;r < n;++r){
  20. for(int c = 0;c < n;++c){
  21. if(!vis[r][c]){ // 访问的格子不再访问,不要放入子函数判断
  22. boolean[] res = bfs(r,c);
  23. if(!res[0]) ++peek;
  24. if(!res[1]) ++bottom;
  25. }
  26. }
  27. }
  28. System.out.printf("%d %d\n",peek,bottom);
  29. }
  30. /*通过BFS进行Flood Fill*/
  31. public static boolean[] bfs(int sr,int sc){
  32. /*标志位: 是否存在比当前格子高的山峰,是否存在比当前格子矮的山峰 */
  33. boolean[] res = new boolean[2];
  34. int row = map.length,col = map[0].length;
  35. Queue<int[]> q = new ArrayDeque<>();
  36. q.offer(new int[]{sr,sc});
  37. vis[sr][sc] = true;
  38. while(!q.isEmpty()){
  39. int[] t = q.poll();
  40. int r = t[0],c = t[1];
  41. for(int i = -1;i <= 1;++i){
  42. for(int j = -1;j <= 1;++j){
  43. if(i == 0 && j == 0) continue;
  44. int rr = i + r,cc = j + c;
  45. if(rr < 0 || rr >= row || cc < 0 || cc >= col) continue;
  46. if(map[rr][cc] > map[r][c]) res[0] = true; // 相邻格子存在比当前高的山峰
  47. else if(map[rr][cc] < map[r][c]) res[1] = true; // 相邻格子存在比当前矮的山峰
  48. else if(!vis[rr][cc]){
  49. q.offer(new int[]{rr,cc});
  50. vis[rr][cc] = true;
  51. }
  52. }
  53. }
  54. }
  55. return res;
  56. }
  57. }