蓝桥杯算法集训 - Week 5:各类DP算法入门

蓝桥杯算法集训 - Week 5

本系列随笔用于整理AcWing题单——《蓝桥杯集训·每日一题2024》的系列题型及其对应的算法模板。

一、DP入门

对应2024蓝桥杯每日一题 Week 5 星期三 线性DP

DP 算法复习参考资料:第 14 章 动态规划 - Hello 算法

闫式DP分析法
闫式DP分析法

Ⅰ、打家劫舍

198. 打家劫舍 - 力扣(LeetCode)

打家劫舍

DP分析法
子状态划分

class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        }
		// 前 i 家户可偷窃收益的最大值
        int[] dp = new int[n];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for (int i = 2; i < n; i++) {
			// 偷窃或不偷取 i 号房子
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[n - 1];
    }
}

Ⅱ、数字三角形

3304. 数字三角形 - AcWing题库

数字三角形

DFS记忆化搜索与DP实现

import java.util.Scanner;

public class Main {
	static final int N = 110;
	static int n;
	static int[][] triangle = new int[N][N], st = new int[N][N], dp = new int[N][N];

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= i; j++) {
				triangle[i][j] = sc.nextInt();
			}
		}
		sc.close();

		System.out.println(dp());
		System.out.println(dfs(1, 1, 1, 1));
	}

	// DP:dp[i][j]表示 0~i 行的第 j 个数字累加的最大值
	static int dp() {
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= i; j++) {
				// 状态转移方程
				dp[i][j] = triangle[i][j] + Math.max(dp[i - 1][j], dp[i - 1][j - 1]);
			}
		}
		if (n % 2 == 0)
			return Math.max(dp[n][n / 2], dp[n][n / 2 + 1]);
		else
			return dp[n][n / 2 + 1];
	}

	// DFS记忆化搜索
	static int dfs(int a, int b, int l, int r) {
		// 到达最后一行
		if (a == n) {
			if (Math.abs(l - r) <= 1) {
				return triangle[a][b];
			}
			return 0;
		}
		// 递归向下搜索
		int leftValue = st[a + 1][b] == 0 ? dfs(a + 1, b, l + 1, r) : st[a + 1][b];
		int rightValue = st[a + 1][b + 1] == 0 ? dfs(a + 1, b + 1, l, r + 1) : st[a + 1][b + 1];
		st[a][b] = triangle[a][b] + Math.max(leftValue, rightValue);
		return st[a][b];
	}
}

Ⅲ、李白打酒

4408. 李白打酒加强版 - AcWing题库

李白打酒

状态转移方程

import java.util.Scanner;

public class Main {
	static final int N = 110, MOD = 1000000007;
	static int n, m;
	static int[][][] dp = new int[N][N][N + 1]; // dp[i][j][k]表示路过了i家酒店,j朵花,还剩k斗酒的合法方案数

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		m = sc.nextInt();
		sc.close();

		// 起始状态方案数为 1
		dp[0][0][2] = 1;
		for (int i = 0; i <= n; i++) {
			for (int j = 0; j <= m; j++) {
				for (int k = 0; k < N; k++) {
					// 当前方案数 + 最后一步遇到的是店的方案数
					if (i > 0 && k % 2 == 0)
						dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j][k / 2]) % MOD;
					// 当前方案数 + 最后一步遇到的是花的方案数
					if (j > 0)
						dp[i][j][k] = (dp[i][j][k] + dp[i][j - 1][k + 1]) % MOD;
				}
			}
		}
		System.out.println(dp[n][m - 1][1]); // 输出最后一步为遇到花并剩1斗的方案数
	}
}

二、状态压缩DP

状压DP复习参考:状态压缩DP学习总结+经典例题精解

Ⅰ、代码模板

// 总状态数
int maxn = 1 << n;

// 枚举已有的集合数:按照状态转移的顺序,一般从小编号到大编号
for(int i = 1; i <= m; ++ i){
    // 枚举当前集合中的状态
    for(int j = 0; j < maxn; ++ j){
        // 判断当前集合是否处于合法状态
        if(check(i, j)){
            for(int k = 0; k < maxn; ++ k){
                // 枚举上一个集合的状态
                if(上一个集合的状态是否合格 + 上一个集合的状态和当前状态的集合是否产生了冲突){
                    // 列写状态转移方程
                }
            }
        }
    }
}

Ⅱ、最短Hamilton路径

91. 最短Hamilton路径 - AcWing题库

最短Hamilton路径

DP分析法
分析引用

import java.util.*;

public class Main {
	public static void main(String args[]) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt(), a[][] = new int[n][n];
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				a[i][j] = sc.nextInt();
			}
		}
		sc.close();
		// DP 状态表示:所有从0走到j,走过的所有点的情况是i的所有路径最小值
		int dp[][] = new int[1 << n][n];
		// 使用二进制状态压缩:走0,2,3这三个点表示为 11010;
		for (int i = 0; i < (1 << n); i++) {
			Arrays.fill(dp[i], (int) 1e9);
		}
		// 初始化旅行状态为 00001,停留在原点的值
		dp[1][0] = 0;
		for (int i = 2; i < (1 << n); i++) {
			if (Integer.bitCount(i) != 1) {
				for (int j = 0; j < n; j++) {
					// 如果 i 的第 j 位为1,即本次状态路过了 j 点时
					if ((i >> j & 1) == 1) {
						// k 表示走到 j 这个点之前,以 k 为终点的最短距离
						for (int k = 0; k < n; k++) {
							if (j != k && (i >> k & 1) == 1) {
								// 状态转移方程:f[i][j]=min(f[i][j], f[i-(1<<j)][k] + w[k][j])
								dp[i][j] = Math.min(dp[i][j], dp[i ^ (1 << j)][k] + a[j][k]);
							}
						}
					}
				}
			}
		}
		System.out.println(dp[(1 << n) - 1][n - 1]); // 表示所有点都走过了,,且终点是 n-1 的最短距离
	}
}

三、背包DP

背包问题复习参考资料:14.4 0-1 背包问题 - Hello 算法

Ⅰ、代码模板

01背包问题

/* 0-1 背包:空间优化后的动态规划 */
int knapsackDPComp(int[] wgt, int[] val, int cap) {
    int n = wgt.length;
    // 初始化 dp 表
    int[] dp = new int[cap + 1];
    // 状态转移
    for (int i = 1; i <= n; i++) {
        // 倒序遍历
        for (int c = cap; c >= 1; c--) {
            if (wgt[i - 1] <= c) {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    return dp[cap];
}

完全背包问题

/* 完全背包:空间优化后的动态规划 */
int unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {
    int n = wgt.length;
    // 初始化 dp 表
    int[] dp = new int[cap + 1];
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int c = 1; c <= cap; c++) {
            if (wgt[i - 1] > c) {
                // 若超过背包容量,则不选物品 i
                dp[c] = dp[c];
            } else {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    return dp[cap];
}

Ⅱ、货币系统

1371. 货币系统 - AcWing题库

货币系统

DP分析法
讲解分析引用

import java.util.Scanner;

public class Main {
	static final int V = 30, N = 10010;
	static int v, n;
	static int[] coins = new int[V];
	static long[][] dp = new long[V][N]; // dp[i][j]表示第 i 种硬币能凑出 j 金额的组合数最大值
	
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		v = sc.nextInt();
		n = sc.nextInt();
		for (int i = 1; i <= v; i++) {
			coins[i] = sc.nextInt();
		}
		sc.close();
		
		dp[0][0] = 1;
		for (int i = 1; i <= v; i++) {
			for (int j = 0; j <= n; j++) {
				for (int k = 0; k * coins[i] <= j; k++) {
					dp[i][j] += dp[i - 1][j - k * coins[i]];
				}
			}
		}
		System.out.println(dp[v][n]);
	}
}

Ⅲ、砝码称重

3417. 砝码称重 - AcWing题库

砝码称重

DP分析法
讲解引用

import java.util.Scanner;

public class Main {
	static final int N = 110;
	static int n, max = 0;
	static int[] w = new int[N];
	static boolean[][] dp = new boolean[N][200010]; // 前 i 个砝码可以称出重量 j 的布尔值
	
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		for (int i = 1; i <= n; i++) {
			w[i] = sc.nextInt();
			max += w[i];
		}
		sc.close();
		
		dp[0][0] = true;
		for (int i = 1; i <= n; i++) {
			for (int j = 0; j <= max; j++) {
				// 不选 || 选了放右边 || 选了放左边
				dp[i][j] = dp[i - 1][j] || dp[i - 1][Math.abs(j - w[i])] || dp[i - 1][j + w[i]];
			}
		}
		
		int res = 0;
		for (int j = 1; j <= max; j++) {
			if (dp[n][j])
				res++;
		}
		System.out.println(res);
	}
}

四、区间DP

区间DP算法博客参考:区间dp(含模板及例题)

Ⅰ、代码模板

for (int len = 1; len <= n; len++) { // 区间长度
    for (int i = 1; i + len - 1 <= n; i++) { // 枚举起点
        int j = i + len - 1; // 区间终点
        if (len == 1) {
            dp[i][j] = 初始值;
            continue;
        }

		// 枚举分割点,构造状态转移方程
        for (int k = i; k < j; k++) {
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
        }
    }
}

Ⅱ、石子合并

282. 石子合并 - AcWing题库

石子合并

DP分析法
分析引用

import java.io.*;

public class Main {
	static final int N = 310;
	static int n;
	static int[] prefix = new int[N];
	static int[][] dp = new int[N][N]; // dp[i][j]表示将区间[i, j]的石子合并成一堆的最小代价

	/**
	 * 282. 石子合并(区间 DP 模版题详解)
	 * 
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		n = Integer.parseInt(br.readLine());
		String[] split = br.readLine().split("\\s+");
		for (int i = 1; i <= n; i++) {
			prefix[i] = Integer.parseInt(split[i - 1]);
			prefix[i] += prefix[i - 1]; // 初始化前缀和数组
		}
		br.close();

		// 枚举长度和左端点
		for (int len = 2; len <= n; len++) {
			for (int i = 1; i + len - 1 <= n; i++) {
				int j = i + len - 1; // 计算右端点
				dp[i][j] = Integer.MAX_VALUE;
				
				// 枚举边界分界点
				for (int k = i; k < j; k++) {
					// min(dp[i][j], 合并左侧的最小代价 + 合并右侧的最小代价 + 合并两侧的代价(区间和))
					dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k + 1][j] + prefix[j] - prefix[i - 1]);
				}
			}
		}
		System.out.println(dp[1][n]); // 区间[1, n]合并的最小代价
	}
}

五、树形DP

算法资料参考:算法与数据结构——树形dp套路(java)

Ⅰ、邻接表模板

在解决树形DP问题时,需要先将树形结构用邻接表表示出来。

初始化邻接表

// head[x] 表示顶点 x 在邻接表中的起始位置
// vertex[idx] 表示索引 idx 处的边的目标顶点
// edge[idx] 表示索引 idx 处的边的权值
// next[idx] 指向与索引 idx 处的边同源顶点的下一条边在 vertex 和 edge 数组中的索引
static int[] head = new int[N], vertex = new int[M], edge = new int[M], next = new int[M];
static int idx = 0;

// 初始化head,置为 -1 即顶点没有出边
Arrays.fill(head, -1);

// 添加有向边 (u, v),权值为 w
static void add(int u, int v, int w) {
    // 设置边和目标节点
    vertex[idx] = v;
	edge[idx] = w;
    // 在表头 u 处插入
    next[idx] = head[u];
	head[u] = idx++;
}

访问从 x 出发的所有边

for (int i = head[x]; i != -1; i = next[i]) {
    // 一条有向边 (u, v),权值为 w
    int v = ver[i], w = edge[i];
}

无向图和有向图的区别在于,前者可以通过将添加一条边 (u, v, w) 看作同时对 (u, v, w) 和 (v, u, w) 两条边调用 add 加入邻接表从而获得无向图。

Ⅱ、没有上司的舞会

285. 没有上司的舞会 - AcWing题库

没有上司的舞会

DP分析
分析引用

import java.util.*;

public class Main {
	static final int N = 6010;
	static int n;
	static int[] h = new int[N];
	static Map<Integer, List<Integer>> map = new HashMap<>();
	static int[][] dp = new int[N][2]; // dp[x][2] 表示选 x 这颗子树且选或不选 x 节点本身的最大快乐指数
	static boolean st[] = new boolean[N]; // st[i] 表示节点 i 是否有父节点

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		for (int i = 1; i <= n; i++) {
			h[i] = sc.nextInt();
		}
		for (int i = 0; i < n - 1; i++) {
			int l = sc.nextInt();
			int k = sc.nextInt();
			st[l] = true;
			add(k, l);
		}
		sc.close();

		// 初始化dp
		for (int i = 1; i <= n; i++) {
			dp[i][1] = h[i];
		}
		// 查找根节点
		int root = 0;
		for (int i = 1; i <= n; i++) {
			if (!st[i]) {
				root = i;
				break;
			}
		}
		// 递归计算树形DP值
		dfs(root);
		System.out.println(Math.max(dp[root][0], dp[root][1]));
	}

	// 递归遍历树结构计算DP
	static void dfs(int x) {
		List<Integer> list = map.get(x);
		if (list == null)
			return;
		
		// 遍历子节点
		for (int son : list) {
			dfs(son);
			dp[x][1] += dp[son][0]; // 选择 x 节点及其子树:累加所有子节点但不选自身的最大指数
			dp[x][0] += Math.max(dp[son][1], dp[son][0]); // 选择子树但不选择 x 自身:累加所有子节点选或不选自身的较大值
		}
	}

	// 通过map构造树结构
	static void add(int a, int b) {
		if (!map.containsKey(a))
			map.put(a, new ArrayList<>());
		map.get(a).add(b);
	}
}

Ⅲ、病毒溯源

3465. 病毒溯源 - AcWing题库

病毒溯源

import java.util.*;

public class Main {
	static final int N = 10010;
	static int n, idx;
	static int[] son = new int[N], st = new int[N];
	static int[] edge = new int[N], ver = new int[N], next = new int[N], head = new int[N]; // 邻接表

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		Arrays.fill(head, -1);
		Arrays.fill(son, -1);
		for (int i = 0; i < n; i++) {
			int m = sc.nextInt();
			while (m-- > 0) {
				int x = sc.nextInt();
				add(i, x, x);
				st[x] = 1;
			}
		}
		sc.close();
		
		// 寻找根节点
		int root = 0;
		for (int i = 0; i < n; i++) {
			if (st[i] != 1) {
				root = i;
				break;
			}
		}
		
		// DFS搜索
		System.out.println(dfs(root));
		System.out.print(root);
		while (son[root] != -1) {
			root = son[root];
			System.out.print(" " + root);
		}
	}

	// DFS遍历树结构
	public static int dfs(int x) {
		int res = 0;
		for (int i = head[x]; i != -1; i = next[i]) {
			int j = ver[i];
			int d = dfs(j);
			// 更新最长路径值
			if (d > res || (d == res && j < son[x])) {
				res = d;
				son[x] = j; // 通过存储下标更新路径
			}
		}
		return res + 1;
	}

	// 邻接表存储树结构
	public static void add(int a, int b, int w) {
		ver[idx] = b;
		edge[idx] = w;
		next[idx] = head[a];
		head[a] = idx++;
	}
}
posted @   TfiyuenLau  阅读(80)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示