在看下面的题目之间,推荐阅读这篇博客,把0,1背包问题讲解的更加通俗易懂一些。0,1背包
Energy Stones
这个题目是Google kickstart 2019 roundB的第二题,为防止再翻墙,特将题目粘贴如下:
题目
解析
中文解析
题目意思:森林中住着一个rocker,这个rocker有很多的能量石,吃了这个能量石,rocker就能得到这个能量石的能量(E
)。但是由于能量石比较硬,吃每个能量石需要一些时间(S
)。能量石的能量会随着时间衰减(衰减速度为L
)。给N个能量石,问怎么吃,能使得rocker得到最多的能量。
解析:因为总能量是一定的,但是一旦开始,所有的能量石都按照自己的衰减速度在流失能量,因此,我们吃的时候,应该先将能量衰减速度比较快的吃掉,避免丢失太多的能量。假设现在有两个石头
石头编号 | S | E | L | 总能量 |
---|---|---|---|---|
i | S_i | E_i | L_i | E_i + (E_j - S_i * L_j) |
j | S_j | E_j | L_j | E_j+ (E_i - S_j * L_i ) |
假设每个石头最后的能量都不会变成0,则由上表可知,先取石头i
时,我们丧失的能量为S_i * L_j
,先取石头j
时,我们丧失的能量为S_j * L_i
。因此,我们可以先按照损失的能量降序对石头进行排序。
class Stone implements Comparable<Stone>{
int seconds;
int energy;
int reduce;
public Stone(int _seconds, int _energy, int _reduce) {
this.seconds = _seconds;
this.energy = _energy;
this.reduce = _reduce;
}
@Override
public int compareTo(Stone other) {
long ans = (long)this.seconds * other.reduce - (long)other.seconds * this.reduce;
if (ans > 0) {
return 1;
}
if (ans < 0) {
return -1;
}
return 0;
}
}
排序之后,我们就能保证每次取的时候,都将流量损失最大的石头给吃了。之后,吃哪个石头,就看我们怎么选了,因为一个石头可能它的能量比较大,但是它又比较浪费时间,这时候,就需要我们进行权衡。(其实这个和背包问题比较像,每次往背包里放从西的时候,它的价值可能比较大,但是它可能非常浪费空间。)。其实,在排序之后,它是0,1背包问题,能用动态规划解决。
动态规划公式:
dp[i][j]
表示在时刻j
, 吃完第i
个石头时能取得的最大价值
- 其中i的取值范围为
[0, N - 1]
j
的取值范围为[0, 100 * 100 + 10]
,(注意: 100 * 100 的原因是,题目中最多有100个石头,
吃每个石头最多用100s,因此,在100 * 100时,一定能吃完所有的石头。)
最后代码如下:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
/**
* dp[i][j]表示到时刻J时,吃完i,能获得的能量
*/
class Stone implements Comparable<Stone>{
int seconds;
int energy;
int reduce;
public Stone(int _seconds, int _energy, int _reduce) {
this.seconds = _seconds;
this.energy = _energy;
this.reduce = _reduce;
}
@Override
public int compareTo(Stone other) {
long ans = (long)this.seconds * other.reduce - (long)other.seconds * this.reduce;
if (ans > 0) {
return 1;
}
if (ans < 0) {
return -1;
}
return 0;
}
}
public class Solution {
private static List<Stone> list = null;
private static int MAX_TIME = 100 * 100 + 1010;
public static void main(String[] args) {
Scanner scanner = new Scanner(new BufferedReader(new InputStreamReader(System.in)));
int caseNum = scanner.nextInt();
for(int i = 0; i < caseNum; i++) {
int stoneNum = scanner.nextInt();
list = new ArrayList<>(stoneNum);
for(int j = 0; j < stoneNum; j++) {
int second = scanner.nextInt();
int energy = scanner.nextInt();
int reduce = scanner.nextInt();
Stone stone = new Stone(second, energy, reduce);
list.add(stone);
}
Collections.sort(list);
// dp[i][j] 表示,在时刻j时,吃完前i个石头获得的总能量
int[][] dp = new int[stoneNum][MAX_TIME];
int ans = 0;
for(int j = list.get(0).seconds; j < MAX_TIME; j++) {
dp[0][j] = Math.max(list.get(0).energy - (j - list.get(0).seconds) * list.get(0).reduce, 0);
ans = Math.max(ans, dp[0][j]);
}
for(int j = 1; j < stoneNum; j++) {
Stone currStone = list.get(j);
// 不取当前石头, 则现在获得的总能量和取前j-1个石头在相同的时间获得的能量相同(可类比0,1背包问题)
for(int k = 0; k < MAX_TIME; k++) {
dp[j][k] = dp[j - 1][k];
}
// 如果取当前的石头,则现在获得的总能量等于,当前石头的能量加上
// 在(k - currStone.seconds)时刻,前(j - 1)个石头获得的能量
for(int k = currStone.seconds; k < MAX_TIME; k++) {
int currEnergy = Math.max(currStone.energy - (k - currStone.seconds) * currStone.reduce, 0);
dp[j][k] = Math.max(dp[j - 1][k - currStone.seconds] + currEnergy, dp[j][k]);
ans = Math.max(ans, dp[j][k]);
}
}
System.out.println("Case #" + (i + 1) + ": " + ans);
}
}
}
上面的代码空间复杂度为O(N)*O(10000)
,时间复杂度也是O(N)*O(10000)
,它的空间复杂度可以优化为O(10000)
,即我们用一维的数组代替二维的数组。仔细观察上边的代码,可以发现,每次计算dp[i][j]
时,只用到了它上一行的两个值,dp[i -1][j]
和dp[i-1][j - x]
,即该列前边的一个数字,因此,我们可以在遍历的时候倒着遍历,这样就不会覆盖之上一行我们需要的值了。照着上边的的代码,我们更改为一维的如下:
一维代码:
package b.EnergyStones;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
class Stone implements Comparable<Stone>{
int seconds;
int energy;
int reduce;
public Stone(int _seconds, int _energy, int _reduce) {
this.seconds = _seconds;
this.energy = _energy;
this.reduce = _reduce;
}
@Override
public int compareTo(Stone other) {
long ans = (long)this.seconds * other.reduce - (long)other.seconds * this.reduce;
if (ans > 0) {
return 1;
}
if (ans < 0) {
return -1;
}
return 0;
}
}
public class Solution {
private static List<Stone> list = null;
private static int MAX_TIME = 100 * 100 + 1010;
public static void main(String[] args) {
Scanner scanner = new Scanner(new BufferedReader(new InputStreamReader(System.in)));
int caseNum = scanner.nextInt();
for(int i = 0; i < caseNum; i++) {
int stoneNum = scanner.nextInt();
list = new ArrayList<>(stoneNum);
for(int j = 0; j < stoneNum; j++) {
int second = scanner.nextInt();
int energy = scanner.nextInt();
int reduce = scanner.nextInt();
Stone stone = new Stone(second, energy, reduce);
list.add(stone);
}
Collections.sort(list);
int[] dp = new int[MAX_TIME];
int ans = 0;
for(int j = 1; j <= stoneNum; j++) {
Stone stone = list.get(j - 1);
for(int k = MAX_TIME - 1; k >= stone.seconds; k--) {
int currE = Math.max(stone.energy - (k - stone.seconds) * stone.reduce, 0);
dp[k] = Math.max(dp[k - stone.seconds] + currE, dp[k]);
ans = Math.max(ans, dp[k]);
}
}
System.out.println("Case #" + (i + 1) + ": " + ans);
}
}
}