0-1背包、有依赖的背包

0-1背包:每个物品只能挑选一次

0-1模板

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.Arrays;

public class Main{

	public static int MAXM = 101;

	public static int MAXT = 1001;

	public static int[] cost = new int[MAXM];

	public static int[] val = new int[MAXM];

	public static int[] dp = new int[MAXT];

	public static int t, n;

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			t = (int) in.nval;
			in.nextToken();
			n = (int) in.nval;
			for (int i = 1; i <= n; i++) {
				in.nextToken();
				cost[i] = (int) in.nval;
				in.nextToken();
				val[i] = (int) in.nval;
			}
			out.println(compute2());
		}
		out.flush();
		out.close();
		br.close();
	}

	// 严格位置依赖的动态规划
	// n个物品编号1~n,第i号物品的花费cost[i]、价值val[i]
	// cost、val数组是全局变量,已经把数据读入了
	public static int compute1() {
		int[][] dp = new int[n + 1][t + 1];
		for (int i = 1; i <= n; i++) {
			for (int j = 0; j <= t; j++) {
				// 不要i号物品
				dp[i][j] = dp[i - 1][j];
				if (j - cost[i] >= 0) {
					// 要i号物品
					dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - cost[i]] + val[i]);
				}
			}
		}
		return dp[n][t];
	}

	// 空间压缩
	public static int compute2() {
		Arrays.fill(dp, 0, t + 1, 0);
		for (int i = 1; i <= n; i++) {
			for (int j = t; j >= cost[i]; j--) {
				dp[j] = Math.max(dp[j], dp[j - cost[i]] + val[i]);
			}
		}
		return dp[t];
	}

}

dp[i][j]表示前i个武器在重量不超过j的情况下所能产生的价值

夏季特惠

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.Arrays;

public class Main {

	public static int MAXN = 501;

	public static int MAXX = 100001;

	// 对于"一定要买的商品",直接买!
	// 只把"需要考虑的商品"放入cost、val数组
	public static int[] cost = new int[MAXN];

	public static long[] val = new long[MAXN];

	public static long[] dp = new long[MAXX];

	public static int n, m, x;

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			m = 1;
			in.nextToken();
			x = (int) in.nval;
			long ans = 0;
			long happy = 0;
			for (int i = 1, pre, cur, well; i <= n; i++) {
				// 原价
				in.nextToken(); pre = (int) in.nval;
				// 现价
				in.nextToken(); cur = (int) in.nval;
				// 快乐值
				in.nextToken(); happy = (long) in.nval;
				well = pre - cur - cur;
				// 如下是一件"一定要买的商品"
				// 预算 = 100,商品原价 = 10,打折后 = 3
				// 那么好处(well) = (10 - 3) - 3 = 4
				// 所以,可以认为这件商品把预算增加到了104!一定要买!
				// 如下是一件"需要考虑的商品"
				// 预算 = 104,商品原价 = 10,打折后 = 8
				// 那么好处(well) = (10 - 8) - 8 = -6
				// 所以,可以认为这件商品就花掉6元!
				// 也就是说以后花的不是打折后的值,是"坏处"
				if (well >= 0) {
					x += well;
					ans += happy;
				} else {
					cost[m] = -well;
					val[m++] = happy;
				}
			}
			ans += compute();
			out.println(ans);
		}
		out.flush();
		out.close();
		br.close();
	}

	public static long compute() {
		Arrays.fill(dp, 0, x + 1, 0);
		for (int i = 1; i <= m; i++) {
			for (int j = x; j >= cost[i]; j--) {
				dp[j] = Math.max(dp[j], dp[j - cost[i]] + val[i]);
			}
		}
		return dp[x];
	}

}

 总优惠金额>=超过预算的金额就是快乐的,那么对于优惠金额大于出售金额的单个物品是必须要选的,并且相差的金额加到原有的预算上就是"心理上"拥有的预算;而对于差值小于0的物品加入待定物品的数组。最后在待定物品的数组在不超过心理上预算的情况下所能选的最大快乐值加之前的快乐值就是最大的快乐值

目标和

public class Code03_TargetSum {

	// 普通尝试,暴力递归版
	public static int findTargetSumWays1(int[] nums, int target) {
		return f1(nums, target, 0, 0);
	}

	// nums[0...i-1]范围上,已经形成的累加和是sum
	// nums[i...]范围上,每个数字可以标记+或者-
	// 最终形成累加和为target的不同表达式数目
	public static int f1(int[] nums, int target, int i, int sum) {
		if (i == nums.length) {
			return sum == target ? 1 : 0;
		}
		return f1(nums, target, i + 1, sum + nums[i]) + f1(nums, target, i + 1, sum - nums[i]);
	}

	// 普通尝试,记忆化搜索版
	public static int findTargetSumWays2(int[] nums, int target) {
		// i, sum -> value(返回值 )
		HashMap<Integer, HashMap<Integer, Integer>> dp = new HashMap<>();
		return f2(nums, target, 0, 0, dp);
	}

	// 因为累加和可以为负数
	// 所以缓存动态规划表用哈希表
	public static int f2(int[] nums, int target, int i, int j, HashMap<Integer, HashMap<Integer, Integer>> dp) {
		if (i == nums.length) {
			return j == target ? 1 : 0;
		}
		if (dp.containsKey(i) && dp.get(i).containsKey(j)) {
			return dp.get(i).get(j);
		}
		int ans = f2(nums, target, i + 1, j + nums[i], dp) + f2(nums, target, i + 1, j - nums[i], dp);
		dp.putIfAbsent(i, new HashMap<>());
		dp.get(i).put(j, ans);
		return ans;
	}

	// 普通尝试
	// 严格位置依赖的动态规划
	// 平移技巧
	public static int findTargetSumWays3(int[] nums, int target) {
		int s = 0;
		for (int num : nums) {
			s += num;
		}
		if (target < -s || target > s) {
			return 0;
		}
		int n = nums.length;
		// -s ~ +s -> 2 * s + 1
		int m = 2 * s + 1;
		// 原本的dp[i][j]含义:
		// nums[0...i-1]范围上,已经形成的累加和是sum
		// nums[i...]范围上,每个数字可以标记+或者-
		// 最终形成累加和为target的不同表达式数目
		// 因为sum可能为负数,为了下标不出现负数,
		// "原本的dp[i][j]"由dp表中的dp[i][j + s]来表示
		// 也就是平移操作!
		// 一切"原本的dp[i][j]"一律平移到dp表中的dp[i][j + s]
		int[][] dp = new int[n + 1][m];
		// 原本的dp[n][target] = 1,平移!
		dp[n][target + s] = 1;
		for (int i = n - 1; i >= 0; i--) {
			for (int j = -s; j <= s; j++) {
				if (j + nums[i] + s < m) {
					// 原本是 : dp[i][j] = dp[i + 1][j + nums[i]]
					// 平移!
					dp[i][j + s] = dp[i + 1][j + nums[i] + s];
				}
				if (j - nums[i] + s >= 0) {
					// 原本是 : dp[i][j] += dp[i + 1][j - nums[i]]
					// 平移!
					dp[i][j + s] += dp[i + 1][j - nums[i] + s];
				}

			}
		}
		// 原本应该返回dp[0][0]
		// 平移!
		// 返回dp[0][0 + s]
		return dp[0][s];
	}

	// 新思路,转化为01背包问题
	// 思考1:
	// 虽然题目说nums是非负数组,但即使nums中有负数比如[3,-4,2]
	// 因为能在每个数前面用+或者-号
	// 所以[3,-4,2]其实和[3,4,2]会达成一样的结果
	// 所以即使nums中有负数,也可以把负数直接变成正数,也不会影响结果
	// 思考2:
	// 如果nums都是非负数,并且所有数的累加和是sum
	// 那么如果target>sum,很明显没有任何方法可以达到target,可以直接返回0
	// 思考3:
	// nums内部的数组,不管怎么+和-,最终的结果都一定不会改变奇偶性
	// 所以,如果所有数的累加和是sum,并且与target的奇偶性不一样
	// 那么没有任何方法可以达到target,可以直接返回0
	// 思考4(最重要):
	// 比如说给定一个数组, nums = [1, 2, 3, 4, 5] 并且 target = 3
	// 其中一个方案是 : +1 -2 +3 -4 +5 = 3
	// 该方案中取了正的集合为A = {1,3,5}
	// 该方案中取了负的集合为B = {2,4}
	// 所以任何一种方案,都一定有 sum(A) - sum(B) = target
	// 现在我们来处理一下这个等式,把左右两边都加上sum(A) + sum(B),那么就会变成如下:
	// sum(A) - sum(B) + sum(A) + sum(B) = target + sum(A) + sum(B)
	// 2 * sum(A) = target + 数组所有数的累加和
	// sum(A) = (target + 数组所有数的累加和) / 2
	// 也就是说,任何一个集合,只要累加和是(target + 数组所有数的累加和) / 2
	// 那么就一定对应一种target的方式
	// 比如非负数组nums,target = 1, nums所有数累加和是11
	// 求有多少方法组成1,其实就是求,有多少种子集累加和达到6的方法,(1+11)/2=6
	// 因为,子集累加和6 - 另一半的子集累加和5 = 1(target)
	// 所以有多少个累加和为6的不同集合,就代表有多少个target==1的表达式数量
	// 至此已经转化为01背包问题了
	public static int findTargetSumWays4(int[] nums, int target) {
		int sum = 0;
		for (int n : nums) {
			sum += n;
		}
		if (sum < target || ((target & 1) ^ (sum & 1)) == 1) {
			return 0;
		}
		return subsets(nums, (target + sum) >> 1);
	}

	// 求非负数组nums有多少个子序列累加和是t
	// 01背包问题(子集累加和严格是t) + 空间压缩
	// dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
	public static int subsets(int[] nums, int t) {
		if (t < 0) {
			return 0;
		}
		int[] dp = new int[t + 1];
		dp[0] = 1;
		for (int num : nums) { // i省略了
			for (int j = t; j >= num; j--) {
				dp[j] += dp[j - num];
			}
		}
		return dp[t];
	}

}

1.二维动态规划:dp[i][j]表示i~最后一个元素在和为j的情况下有多少种方法使得和为target。

注:和可能有负数的情况,所以为了数组访问,可以将和平移为正数,看上述代码

2.对于整数集合x和负数集合y,x+y为sum,x-y为target,解得x=(sum+target)/2,所以问题就转化为在数组中选数字直到和为x的个数

最后一块石头的重量

public class Code04_LastStoneWeightII {

	public static int lastStoneWeightII(int[] nums) {
		int sum = 0;
		for (int num : nums) {
			sum += num;
		}
		// nums中随意选择数字
		// 累加和一定要 <= sum / 2
		// 又尽量接近
		int near = near(nums, sum / 2);
		return sum - near - near;
	}

	// 非负数组nums中,子序列累加和不超过t,但是最接近t的累加和是多少
	// 01背包问题(子集累加和尽量接近t) + 空间压缩
	public static int near(int[] nums, int t) {
		int[] dp = new int[t + 1];
		for (int num : nums) {
			for (int j = t; j >= num; j--) {
				// dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-nums[i]]+nums[i])
				dp[j] = Math.max(dp[j], dp[j - num] + num);
			}
		}
		return dp[t];
	}

}

就是在背包容量为石头总重量的一半的情况下,所能选的最大重量

预算方案


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.Arrays;

public class Main {

	public static int MAXN = 33001;

	public static int MAXM = 61;

	public static int[] cost = new int[MAXM];

	public static int[] val = new int[MAXM];

	public static boolean[] king = new boolean[MAXM];

	public static int[] fans = new int[MAXM];

	public static int[][] follows = new int[MAXM][2];

	public static int[] dp = new int[MAXN];

	public static int n, m;

	public static void clean() {
		for (int i = 1; i <= m; i++) {
			fans[i] = 0;
		}
	}

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			in.nextToken();
			m = (int) in.nval;
			clean();
			for (int i = 1, v, p, q; i <= m; i++) {
				in.nextToken(); v = (int) in.nval;
				in.nextToken(); p = (int) in.nval;
				in.nextToken(); q = (int) in.nval;
				cost[i] = v;
				val[i] = v * p;
				king[i] = q == 0;
				if (q != 0) {
					follows[q][fans[q]++] = i;
				}
			}
			out.println(compute2());
		}
		out.flush();
		out.close();
		br.close();
	}

	// 严格位置依赖的动态规划
	public static int compute1() {
		// dp[0][....] = 0 : 无商品的时候
		int[][] dp = new int[m + 1][n + 1];
		// p : 上次展开的主商品编号
		int p = 0;
		for (int i = 1, fan1, fan2; i <= m; i++) {
			if (king[i]) {
				for (int j = 0; j <= n; j++) {
					// dp[i][j] : 0...i范围上,只关心主商品,并且进行展开
					//            花费不超过j的情况下,获得的最大收益
					// 可能性1 : 不考虑当前主商品
					dp[i][j] = dp[p][j];
					if (j - cost[i] >= 0) {
						// 可能性2 : 考虑当前主商品,只要主
						dp[i][j] = Math.max(dp[i][j], dp[p][j - cost[i]] + val[i]);
					}
					// fan1 : 如果有附1商品,编号给fan1,如果没有,fan1 == -1
					// fan2 : 如果有附2商品,编号给fan2,如果没有,fan2 == -1
					fan1 = fans[i] >= 1 ? follows[i][0] : -1;
					fan2 = fans[i] >= 2 ? follows[i][1] : -1;
					if (fan1 != -1 && j - cost[i] - cost[fan1] >= 0) {
						// 可能性3 : 主 + 附1
						dp[i][j] = Math.max(dp[i][j], dp[p][j - cost[i] - cost[fan1]] + val[i] + val[fan1]);
					}
					if (fan2 != -1 && j - cost[i] - cost[fan2] >= 0) {
						// 可能性4 : 主 + 附2
						dp[i][j] = Math.max(dp[i][j], dp[p][j - cost[i] - cost[fan2]] + val[i] + val[fan2]);
					}
					if (fan1 != -1 && fan2 != -1 && j - cost[i] - cost[fan1] - cost[fan2] >= 0) {
						// 可能性5 : 主 + 附1 + 附2
						dp[i][j] = Math.max(dp[i][j],
								dp[p][j - cost[i] - cost[fan1] - cost[fan2]] + val[i] + val[fan1] + val[fan2]);
					}
				}
				p = i;
			}
		}
		return dp[p][n];
	}

	// 空间压缩
	public static int compute2() {
		Arrays.fill(dp, 0, n + 1, 0);
		for (int i = 1, fan1, fan2; i <= m; i++) {
			if (king[i]) {
				for (int j = n; j >= cost[i]; j--) {
					dp[j] = Math.max(dp[j], dp[j - cost[i]] + val[i]);
					fan1 = fans[i] >= 1 ? follows[i][0] : -1;
					fan2 = fans[i] >= 2 ? follows[i][1] : -1;
					if (fan1 != -1 && j - cost[i] - cost[fan1] >= 0) {
						dp[j] = Math.max(dp[j], dp[j - cost[i] - cost[fan1]] + val[i] + val[fan1]);
					}
					if (fan2 != -1 && j - cost[i] - cost[fan2] >= 0) {
						dp[j] = Math.max(dp[j], dp[j - cost[i] - cost[fan2]] + val[i] + val[fan2]);
					}
					if (fan1 != -1 && fan2 != -1 && j - cost[i] - cost[fan1] - cost[fan2] >= 0) {
						dp[j] = Math.max(dp[j],
								dp[j - cost[i] - cost[fan1] - cost[fan2]] + val[i] + val[fan1] + val[fan2]);
					}
				}
			}
		}
		return dp[n];
	}

}

设置king数组记录物品是否为主物品,fans数组记录主物品的附属物品的数量,follows二维数组记录主物品下附属品的编号。然后和背包问题一样,只遍历主物品并讨论展开其附属品的情况。时间复杂度为O(m*n),

给定一个非负数数组和一个正数k,返回所有子序列中累加和最小的前k个累加和

import java.util.ArrayList;
import java.util.Arrays;
import java.util.PriorityQueue;

// 非负数组前k个最小的子序列累加和
// 给定一个数组nums,含有n个数字,都是非负数
// 给定一个正数k,返回所有子序列中累加和最小的前k个累加和
// 子序列是包含空集的
// 1 <= n <= 10^5
// 1 <= nums[i] <= 10^6
// 1 <= k <= 10^5
// 注意这个数据量,用01背包的解法是不行的,时间复杂度太高了
// 对数器验证
public class Code06_TopKMinimumSubsequenceSum {

	// 暴力方法
	// 为了验证
	public static int[] topKSum1(int[] nums, int k) {
		ArrayList<Integer> allSubsequences = new ArrayList<>();
		f1(nums, 0, 0, allSubsequences);
		allSubsequences.sort((a, b) -> a.compareTo(b));
		int[] ans = new int[k];
		for (int i = 0; i < k; i++) {
			ans[i] = allSubsequences.get(i);
		}
		return ans;
	}

	// 暴力方法
	// 得到所有子序列的和
	public static void f1(int[] nums, int index, int sum, ArrayList<Integer> ans) {
		if (index == nums.length) {
			ans.add(sum);
		} else {
			f1(nums, index + 1, sum, ans);
			f1(nums, index + 1, sum + nums[index], ans);
		}
	}

	// 01背包来实现
	// 这种方法此时不是最优解
	// 因为n很大,数值也很大,那么可能的累加和就更大
	// 时间复杂度太差
	public static int[] topKSum2(int[] nums, int k) {
		int sum = 0;
		for (int num : nums) {
			sum += num;
		}
		// dp[i][j]
		// 1) dp[i-1][j]
		// 2) dp[i-1][j-nums[i]
		int[] dp = new int[sum + 1];
		dp[0] = 1;
		for (int num : nums) {
			for (int j = sum; j >= num; j--) {
				dp[j] += dp[j - num];
			}
		}
		int[] ans = new int[k];
		int index = 0;
		for (int j = 0; j <= sum && index < k; j++) {
			for (int i = 0; i < dp[j] && index < k; i++) {
				ans[index++] = j;
			}
		}
		return ans;
	}

	// 正式方法
	// 用堆来做是最优解,时间复杂度O(n * log n) + O(k * log k)
	public static int[] topKSum3(int[] nums, int k) {
		Arrays.sort(nums);
		// (子序列的最右下标,子序列的累加和)
		PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[1] - b[1]);
		heap.add(new int[] { 0, nums[0] });
		int[] ans = new int[k];
		for (int i = 1; i < k; i++) {//空集算一个
			int[] cur = heap.poll();
			int right = cur[0];
			int sum = cur[1];
			ans[i] = sum;
			if (right + 1 < nums.length) {
				heap.add(new int[] { right + 1, sum - nums[right] + nums[right + 1] });
				heap.add(new int[] { right + 1, sum + nums[right + 1] });
			}
		}
		return ans;
	}

1.0-1背包问题:求出所有和的子序列个数,取出前k个小的和,但会超时

2.优先级队列:在数组中元素全部为非负数的情况下,先将数组从小到大排序,可以先加入数值最小的数a,然后将其弹出,将其排序后数组右边的数b及a+b加入到小根堆里直至弹出的数量到k

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值