算法笔记之动态规划(DP)

写在前面

因为算法课上骆老师讲的是真的好,所以对动态规划还是比较熟悉的。总结来说就是自底向上求解,主要在于DP转移方程的分析,然后构造DP数组进行填表即可,有时可能需要注意保存求解路径。

动态规划

简单分析一个最大连续子序列之和问题:

给定K个整数的序列{ N1, N2, ..., NK },其任意连续子序列可表示为{ Ni, Ni+1, ..., Nj },
其中 1 <= i <= j <= K。最大连续子序列是所有连续子序中元素和最大的一个, 
例如给定序列{ -2, 11, -4, 13, -5, -2 },其最大连续子序列为{ 11, -4, 13 },最大和为20。
  • 蛮力法求解,就是遍历任意两个数,对其形成的子序列进行求和。
  • 动态规划自底向上求解。如下图,构造DP数组,求解过程就是填表过程,从最底层(m(i,i))开始,然后依次向上沿对角线进行求解,每次都会用到之前已经计算过的结果。这个例子还是很简单的,因为DP转移方程很明确。
  • 就这道题而言,还有一个O(n)的解法,参考动态规划经典五题
    在这里插入图片描述
    动态规划代码实现
	public static void dp(int[] seq) {
		int n = seq.length;
		int[][] dparr = new int[n][];
		int sum = 0;
		int start = 0;
		int end = 0;
		// 最底层(最外列)求解
		for (int i = 0; i < n; i++) {
			dparr[i] = new int[i + 1];
			dparr[i][i] = seq[i];
			if (dparr[i][i] > sum) {
				sum = dparr[i][i];
				start = i;
				end = i;
			}
		}
		// 自底向上求解
		for (int i = 1; i < n; i++) {
			for (int j = 0; j < n - i; j++) {
				// 在原有的基础上进行计算
				// 转移方程
				dparr[j + i][j] = dparr[j + i - 1][j] + seq[j + i];
				if (dparr[j + i][j] > sum) {
					sum = dparr[j + i][j];
					start = j;
					end = j + i;
				}
			}
		}

		System.out.println("maxsum = " + sum);
		for (int i = start; i <= end; i++) {
			System.out.print(seq[i] + " ");
		}
	}

O(n)复杂度代码

	public static void maxsum(int[] seq) {
		int maxSum = 0, thisSum = 0;
		for (int i = 0; i < seq.length; i++) {
			thisSum += seq[i];
			if (thisSum > maxSum) {
				maxSum = thisSum;
			} else if (thisSum < 0) {
				thisSum = 0;
			}
		}
		System.out.println("\nmaxsum = " + maxSum);
	}
取球博弈
桌子上有一堆石头,每一次你们轮流取1至3颗石头。最后一个取走石头的人就是赢家。第一轮由你先取。
根据题设条件:
当n∈[1,3]时,先手必胜。
当n == 4时,无论先手第一轮如何选取,下一轮都会转化为n∈[1,3]的情形,此时先手必负。
当n∈[5,7]时,先手必胜,先手分别通过取走[1,3]颗石头,可将状态转化为n == 4时的情形,此时后手必负。
当n == 8时,无论先手第一轮如何选取,下一轮都会转化为n∈[5,7]的情形,此时先手必负。 
……
以此类推,可以得出结论:
当n % 4 != 0时,先手必胜;否则先手必负。
两个人玩取球的游戏。
一共有N个球,每人轮流取球,每次可取集合{n1,n2,n3}中的任何一个数目。
如果无法继续取球,则游戏结束。
此时,持有奇数个球的一方获胜。
如果两人都是奇数,则为平局。

假设双方都采用最聪明的取法,
第一个取球的人一定能赢吗?
试编程解决这个问题。

输入格式:
第一行3个正整数n1 n2 n3,空格分开,表示每次可取的数目 (0<n1,n2,n3<100)
第二行5个正整数x1 x2 ... x5,空格分开,表示5局的初始球数(0<xi<1000)

输出格式:
一行5个字符,空格分开。分别表示每局先取球的人能否获胜。
能获胜则输出+,
次之,如有办法逼平对手,输出0,
无论如何都会输,则输出-
例如,输入:
1 2 3
1 2 3 4 5
程序应该输出:
+ 0 + 0 -
再例如,输入:
1 4 5
10 11 12 13 15
程序应该输出:
0 - 0 + +
再例如,输入:
2 3 5
7 8 9 10 11
程序应该输出:
+ 0 0 0 0

解题思路是减治法+动态规划
动态规划的应用在于,可以构建两个玩家的DP数组,table[i][j]保存两个玩家当前分别有i个球和j个球时的胜负情况;
减治法,实际上是一种递归的实现,初始时是最上层的情况,然后不断递归,到可以判断胜负为止(即拈游戏中球数目为4时)。

代码实现
/**
 * 拈游戏 n个石头, 两个人每次取1-m个, 取走最有一个石头的玩家获胜. 
 * 减治法思想, 1<=n<=m时, 先手获胜; n=m+1时, 先手失败;
 * m+2<=n<=2m+1, 先手获胜; n=2m+2时, 先手失败; 
 * 以此类推, n%(m+1)!=0时, 先手获胜
 * 
 * 取球博弈 n个球, 每次可取{n1, n2, n3}中任意值数目球 
 * 最后, 持有奇数个球的一方获胜; 都是奇数则平局;
 * 解决思路, 减值法和动态规划 为两个玩家分别建立一个DP表,
 *  (i,j)保存玩家1拥有i个球、玩家2拥有j个球时的结果 
 * 结果中, +表示当前玩家获胜,-表示失败, 0表示平局 递归进行解的搜索
 * 
 *
 */
public class Main {

	// 每次可以取得球数目
	static int[] ns = new int[3];
	// 五局初始的球数目
	static int[] xs = new int[5];
	// 分别保存两个玩家的动态表
	static char[][][] table;
	static int min;

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner in = new Scanner(System.in);
		for (int i = 0; i < 3; i++) {
			ns[i] = in.nextInt();
		}

		for (int i = 0; i < 5; i++) {
			xs[i] = in.nextInt();
		}
		getMin();
		int k;
		for (int i: xs) {
			table = new char[2][1000][1000];
			k = dp(0, 0, 0, i);
			System.out.print((char)k + " ");
		}
	}

	public static void getMin() {
		int m = 99999;
		for (int n : ns) {
			if (n < m) {
				m = n;
			}
		}
		min = m;
	}

	// 当前的玩家
	// 两个玩家分别拥有的球数目
	// 初始球数目
	// 终止条件为当前球数小于最小可取球数
	// 否则继续递归
	public static char dp(int p, int aowns, int bowns, int s) {
		int rest = s - aowns - bowns;
		// 已经计算过了则直接返回
		if (table[p][aowns][bowns] != '\u0000') {
			return table[p][aowns][bowns];
		}

		// 如果没法再取球则计算双方胜败
		if (rest < min) {
			if (aowns % 2 == 1 && bowns % 2 == 0) {
				table[p][aowns][bowns] = '+';
			} else if (aowns % 2 == 0 && bowns % 2 == 1) {
				table[p][aowns][bowns] = '-';
			} else {
				table[p][aowns][bowns] = '0';
			}
			return table[p][aowns][bowns];

		}
		if (p == 0) {
			boolean flag = false;
			for (int i : ns) {
				if (rest >= i) {
					if (dp(1, aowns + i, bowns, s) == '+') {
						return table[0][aowns][bowns] = '+';
					} else if (dp(1, aowns + i, bowns, s) == '0') {
						flag = true;
					}
				}
			}
			if (flag) {
				table[0][aowns][bowns] = '0';
			} else {
				table[0][aowns][bowns] = '-';
			}
		}
		if (p == 1) {
			boolean flag = false;
			for (int i : ns) {
				if (rest >= i) {
					if (dp(0, aowns, bowns + i, s) == '-') {
						return table[1][aowns][bowns] = '-';
					} else if (dp(0, aowns + i, bowns, s) == '0') {
						flag = true;
					}
				}
			}
			if (flag) {
				table[1][aowns][bowns] = '0';
			} else {
				table[1][aowns][bowns] = '+';
			}
		}

		return table[p][aowns][bowns];
	}
}

Reference

动态规划经典五题
leetcode_292_拈游戏
第七届蓝桥杯取球博弈详解
算法-动态规划 Dynamic Programming–从菜鸟到老鸟

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值