算法训练——动态规划

今天要跟大家分享的是关于动态规划的一些例题,因为动态规划是算法中非常重要的一部分,掌握其思想是必要的,只有多刷题才能更好地理解并掌握它

🎈1.骰子的点数

题目描述:
将一个骰子投掷 n 次,获得的总点数为 s,s 的可能范围为 n∼6n。掷出某一点数,可能有多种掷法,例如投掷 2 次,掷出 3
点,共有 [1,2],[2,1] 两种掷法。请求出投掷 n 次,掷出 n∼6n 点分别有多少种掷法。
题目链接:⚡骰子的点数

思路:
对于每个骰子,它出现的点数无非是1~6,所以第 i 次投出 j 点的掷法为前一次(i-1次)投出 j-1,j-2,j-3,j-4,j-5,j-6 的掷法之和(前提是 j - k > 0, k就是1~6),用dp[i][j]表示投 i 次掷出点数 j 的所有方案,所以可以得到状态转移方程:
dp[i][j] = dp[i][j] + dp[i-1][j-k] (1<=k<=6,j-k > 0)

代码:

class Solution {
    int[][] dp = new int[11][61]; //定义dp数组最大为dp[10][60]
    public int[] numberOfDice(int n) {
		for (int i = 1; i <= 6; ++i) {//初态:扔一次出现1~6的种数都为1,
			dp[1][i] = 1;			
		}
		for (int i = 2; i <= n; ++i) {//初态扔一次有了,就从扔两次开始计算
			for (int j = i; j <= i * 6; j++) {//范围为:(n~6*n)
				//每一次都能投出6种点数,上一次加这一次的点数等于j的情况都要加到dp[i][j]上
				for (int k = 1; k <= 6; k++) {
					if (j-k>0) //前一个骰子不能投出点数0
						dp[i][j] += dp[i-1][j-k];
				}
			}
		}
		return Arrays.copyOfRange(dp[n], n, 6*n+1);
    }
}

🎈2.挖地雷

题目描述:
在一个地图上有 n个地窖(n≤200),每个地窖中埋有一定数量的地雷。
同时,给出地窖之间的连接路径,并规定路径都是单向的,且保证都是小序号地窖指向大序号地窖,也不存在可以从一个地窖出发经过若干地窖后又回到原来地窖的路径。
某人可以从任意一处开始挖地雷,然后沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。
设计一个挖地雷的方案,使他能挖到最多的地雷。
题目链接:⚡挖地雷

思路:
1.用一维数组num来存放地雷数,dp存放到达各点的最大地雷数,pre记录前一个点,二维数组road记录两结点之间是否能到达

2.找初态,初态就是dp各点都为自身地雷数num,因为有的点可能是起点,例如题中样例的最大地雷数起点为3而不是1

3.找状态转移,首先,road[i][j] 为真,两点才能到达,其次,要dp[i]+num[j]>dp[j] 的情况才能使 dp[j]=dp[i]+num[j],因为有的dp[i]+num[j]<dp[j],此时就不能改变dp[j]。

4.每更新一次dp[j]就更新一次最大地雷数max和max对应的最后一个结点node,最后递归输出pre[node]和max即可

代码:

import java.util.Scanner;
public class Main {
	static Scanner sc = new Scanner(System.in);
	public static void main(String[] args) throws Exception {
		int n = sc.nextInt();
		int[] num = new int[n+1];//记录各个地窖的地雷个数
		int[] dp = new int[n+1];
		boolean[][] road = new boolean[n+1][n+1];//判断两点之间是否能到达
		int[] pre = new int[n+1];//记录到达i点的前一个点
		
		init(n, num, dp, road);//初始化操作
		
		int max = dp[1], node = 1;	//node是挖到最大地雷数的最后一个点,max是最大地雷数
		for (int i = 1; i < n; ++i) {//dp操作
			for (int j = i + 1; j <= n; ++j) {
				if (road[i][j]) {	
					int temp = dp[i] + num[j];//能到该点的地雷数加上该点地雷数num[j]
					if (temp > dp[j]) {//所以只有大于dp[j]才能改变dp[j]
						dp[j] = temp;
						pre[j] = i;//更新pre[j]
					}
				}
				if(dp[j]>max){
					node = j;	//更新node和max
					max = dp[j];
				}
			}
		}
		print(node, pre);//打印结点操作
		System.out.println("\n"+max);
	}
	public static void init(int n, int[] num, int[] dp, boolean[][] road) {
		for (int i = 1; i <= n; ++i) {//初始化dp[]和地雷数
			num[i] = sc.nextInt();
			dp[i] = num[i];
		}
		sc.nextLine();//上一次接收的为数字,下一次为字符串时,需要加sc.nextLine()避免类型冲突
		String tmp="";
		while (!(tmp=sc.nextLine()).equals("0 0")) {//初始化两点之间是否能到达
			String[] ss = tmp.split(" ");
			int x = Integer.parseInt(ss[0]);
			int y = Integer.parseInt(ss[1]);
			road[x][y] = true;
		}
	}
	public static void print(int node, int[] pre) {//巧用递归反向打印结点
		if (pre[node]==0) {
			System.out.print(node);
			return;
		}
		print(pre[node], pre);
		System.out.print("-"+node);
	}
}	

🎈3.旅行

题目描述: 爱丽丝和鲍勃想去旅行。
他们每个人制定了一条旅行路线,每条路线包含一个按给定顺序访问的城市列表,一个城市可能会多次出现在同一路线中。
因为他们想要一起去旅行,所以必须在旅行路线上达成一致。 他们两个都不想改变他们的路线上的城市顺序或者在路线上额外添加城市。
因此,他们只能移除各自路线中的一些城市,使得旅行路线达成一致,并且尽可能的长。 该地区共有 26 个城市,用小写字母 a 到 z 表示。
输入格式:
输入包含两行,第一行是爱丽丝的路线城市列表,
第二行是鲍勃的路线城市列表。每个列表由 1 到 80 个小写字母组成,其间没有空格。
输出格式: 按升序顺序输出所有满足条件的路线列表。
题目链接:⚡旅行

思路:
这跟LCS简直是一模一样,但是这里要求输出所有的能达到最长长度的结果,这一点是难点。
1.用dp得到各个dp[i][j]的值,并得到最大长度max
2.通过DFS去找所有能达到最大长度的子序列,用Java中TreeSet集合元素有序的特点来存放每个最长子序列,最后输出TreeSet的元素即可

代码:

import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
public class Main {
	static Set<String> set = new TreeSet<>();
	static char[] s1,s2;	//存放输入的两个串
	static char[] tmp = new char[80];//存放结果字符串的数组
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		s1 = sc.nextLine().toCharArray();
		s2 = sc.nextLine().toCharArray();
		int[][] dp = new int[s1.length+1][s2.length+1];
		int maxlen = init(dp);//初始化dp,找到最大长度
		addStr(dp, 0, maxlen, 1, 1);//DFS去找字串并加入set
		for (String s : set)
			System.out.println(s);
	}
	public static int init(int[][] dp) {
		int max = 0;
		for (int i = 1; i <= s1.length; i++) {				//初始化dp数组,并返回最大长度的方法
			for (int j = 1; j <= s2.length; j++) {
				if (s1[i-1]==s2[j-1]) 
					dp[i][j] = dp[i-1][j-1] + 1;
				else 
					dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
				max = dp[i][j]>max ? dp[i][j] : max;
			}
		}
		return max;
	}
	//DFS去找能达到最大长度的子序列
	//其中形参k是当前找的子序列长度
	public static void addStr(int[][] dp, int k, int max, int x, int y) {
		if (k==max) {//达到最大长度就可以加入
			set.add(new String(tmp).trim());
			return;
		}
		if (x>s1.length||y>s2.length) //边界条件
			return;
		if (s1[x-1]==s2[y-1]) {//若s1[x]和s2[y]相等就直接进入下一层,可以剪枝
			tmp[k] = s1[x-1];
			addStr(dp, k+1, max, x+1, y+1);
		}else {//不相等就要去扫描下一个相等的元素的x,y是多少
			for (int i = x; i <= s1.length; i++) {
				for (int j = y; j <= s2.length; j++) {//dp[i][j]==k+1很重要,可以剪枝
					if (dp[i][j]==k+1&&s1[i-1]==s2[j-1]) {//因为下一个串一定在上一个长度的基础上+1
						tmp[k] = s1[i-1];    //若dp[i][j]<=k,表示他们的最长子序列长度才达到k,肯定不满足
						addStr(dp, k+1, max, i+1, j+1);
					}
				}
			}
		}
	}
}

🎈4.关路灯

题目描述:
某一村庄在一条路线上安装了 n盏路灯,每盏灯的功率有大有小(即同一段时间内消耗的电量有多有少)。老张就住在这条路中间某一路灯旁,他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。
为了给村里节省电费,老张记录下了每盏路灯的位置和功率,他每次关灯时也都是尽快地去关,但是老张不知道怎样去关灯才能够最节省电。他每天都是在天亮时首先关掉自己所处位置的路灯,然后可以向左也可以向右去关灯。开始他以为先算一下左边路灯的总功率再算一下右边路灯的总功率,然后选择先关掉功率大的一边,再回过头来关掉另一边的路灯,而事实并非如此,因为在关的过程中适当地调头有可能会更省一些。
现在已知老张走的速度为1m/s,每个路灯的位置(是一个整数,即距路线起点的距离,单位:m)、功率(W),老张关灯所用的时间很短而可以忽略不计。
请你为老张编一程序来安排关灯的顺序,使从老张开始关灯时刻算起所有灯消耗电最少(灯关掉后便不再消耗电了)。
**题目链接:**⚡关路灯

思路:
根据题意可知这题是区间DP类型的题,先上模板:

for (int len = 2; len <= n; len++) {//先枚举区间长度
	for (int i = 1; i+len-1 <= n; i++) {//再枚举区间左端点,左端点加区间长度为右端点,不能大于n
		int j = i+len-1;	//区间右端点
		for (int k = i; k < j; k++) {	//枚举区间分割点
			dp[i][j] = Math.min(dp[i][j], dp[i][k]+dp[k+1][j]+合并区间的消耗);
		}
	}
}

然后仔细去分析:关完区间[ i , j ]后,老张要么在 i 点,要么在 j 点,为记录他的位置,可以增加一维空间dp[i][j][0]表示关完[ i , j ]后站在 i 点,dp[ i ][ j ][ 1 ]表示关完[ i , j ]后在 j 点,又发现区间[ i , j ]一定是[ i+1 , j ]或者[ i ][ j-1 ]走来的,所以得到状态转移方程:
f[i][j][0] = min(f[i+1][j][0] + 对应消耗, f[i+1][j][1] + 对应消耗);
f[i][j][1] = min(f[i][j-1][0] + 对应消耗, f[i][j-1][1] + 对应消耗);

for (int len = 2; len <= n; len++) {         // 区间长度
    for (int i = 1; i + len - 1 <= n; i++) { // 区间起点
        int j = i + len - 1;                 // 区间终点
        f[i][j][0]=min(f[i+1][j][0]+对应消耗,f[i+1][j][1]+对应消耗);
        f[i][j][1]=min(f[i][j-1][0]+对应消耗,f[i][j-1][1]+对应消耗);
    }
}

完整代码:

import java.util.Scanner;
public class Main {
	static int[][][] dp = new int[51][51][2];
	static int[] po = new int[51];
	static int[] sum = new int[51];
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt(), c = sc.nextInt();
		for (int i = 1; i <= n; i++) {
			po[i] = sc.nextInt();
			sum[i] = sum[i-1] + sc.nextInt();//计算前缀和
		}
		for (int i = 1; i <= n; i++) 
			for (int j = 1; j <= n; j++)//将dp数组都初始化为一个超大的值,因为我们求的是最小功耗
				dp[i][j][0]=dp[i][j][1] = Integer.MAX_VALUE/3;
		
		dp[c][c][0] = dp[c][c][1] = 0;	//初始位置的功耗为0
 
		for (int len = 2; len <= n; len++) {
			for (int i = 1; i+len-1 <= n; i++) {
				int j = i+len-1;				//count方法用来计算合并消耗
				dp[i][j][0] = Math.min(dp[i+1][j][0] + count(i, i+1, i+1, j, n), 
						dp[i+1][j][1] + count(i, j, i+1, j, n));
				dp[i][j][1] = Math.min(dp[i][j-1][0] + count(i, j, i, j-1, n),
						dp[i][j-1][1] + count(j-1, j, i, j-1, n));
			}
		}
		System.out.println(Math.min(dp[1][n][0], dp[1][n][1]));
	}
	/**
	 * @param x, y  移动前、后的路灯位置
	 * @param l, r  l~r的路灯都是关着的
	 * @param n		总路灯数
	 * @return (po[y]-po[x])相当于时间,(sum[n]-sum[r]+sum[l-1])为其他未关的灯的总功率
	 */
	public static int count(int x, int y, int l, int r, int n) {
		return (po[y]-po[x]) * (sum[n]-sum[r]+sum[l-1]);
	}
}

如果觉得有帮助可以点个赞,感谢!!!😘😘😘

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Easenyang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值