动态规划
动态规划适用于没有更好算法的问题。这些问题只能蛮力穷举,但是还具有最优子问题性质,即穷举时会用到很多重复的子问题,这些子问题可以先行计算并存储以优化计算时间。
背包问题
01背包
二维DP
优点是可以通过回溯拿到最优方案对应的解
递推方程:
f
(
n
,
r
e
s
t
)
=
{
f
(
n
+
1
,
rest
)
,
rest
<
w
(
n
)
;
max
{
f
(
n
+
1
,
rest
)
,
f
(
n
+
1
,
rest
−
w
(
n
)
)
+
v
(
n
)
}
,
rest
≥
w
(
n
)
.
\begin{equation*} f(n, rest)= \begin{cases} f(n+1,\text{rest}),&\text{rest}<w(n);\\ \max \{f(n+1,\text{rest}), f(n+1,\text{rest}-w(n))+v(n)\},&\text{rest} \ge w(n). \end{cases} \end{equation*}
f(n,rest)={f(n+1,rest),max{f(n+1,rest),f(n+1,rest−w(n))+v(n)},rest<w(n);rest≥w(n).
以打表减少递归计算的动态规划初衷出发,我们可以写出以上递推方程。其中
n
n
n表示物品
n
n
n的决策阶段,rest表示这次决策时所剩背包资源容量。
首先创建dp表
import numpy as np
dp = np.zeros((N+1, bag+1)) # 物品0base计数,下标n表示所有决策进行后的边界。
首先对递归边界进行初始化,然后回归计算累加价值即可。
初始化
for rest in range(0, bag+1):
dp[n, rest] = 0 # 决策边界之外任何剩余空间都无法带来价值
回归计算
从二维递推关系中看出我们需要先计算所有
f
(
n
+
1
,
∗
)
f(n+1,*)
f(n+1,∗),再计算
f
(
n
,
∗
)
f(n,*)
f(n,∗),因此我们可以确定内层循环为rest,外层为n。
for n in range(N-1, -1, -1):
wn = w[n]
vn = v[n]
for rest in range(0, bag+1):
if rest<wn:
dp[n, rest] = dp[n+1, rest]
else:
dp[n, rest] = dp[n+1, rest] if dp[n+1, rest]>(dp[n+1, rest-wn]+vn) else dp[n+1, rest-wn]+vn
返回结果
print(dp[0, bag]) # n=0的决策结束后,dp表中存储的即为给定条件下的最大价值。
滚动dp优化打表维度
优点是可以空间复杂度更低。时间上也可以进行一定优化(不能优化渐进复杂度)。
通过观察递推公式与回归计算,可以发现
n
n
n这个维度可以只存在于循环控制中,在dp中通过时间而非空间存储。
因为内层循环为rest,dp[n, rest]的更新要么依赖dp[n, rest]要么依赖dp[n, rest-wn]。
而
r
e
s
t
≤
r
e
s
t
,
r
e
s
t
−
w
n
≤
r
e
s
t
rest \le rest, rest-wn \le rest
rest≤rest,rest−wn≤rest,即存在“单调性”。
因此我们在更新
d
p
[
n
,
r
e
s
t
i
]
dp[n, rest_i]
dp[n,resti]时只需要确保
d
p
[
n
−
1
,
r
e
s
t
j
]
dp[n-1, rest_j]
dp[n−1,restj],
j
≤
i
j \le i
j≤i都可被正确检索即可。
所以内层循环rest需要从bag向0步进,每次只更新最大的未更改值。
import numpy as np
dp = np.zeros((bag+1)) # 物品0base计数,下标n表示所有决策进行后的边界。
dp[rest] = 0 # 决策边界之外任何剩余空间都无法带来价值
for n in range(N-1, -1, -1):
wn = w[n]
vn = v[n]
for rest in range(bag, -1, -1):
if rest>=wn and dp[rest]<(dp[rest-wn]+vn):
dp[rest] = dp[rest-wn]+vn
print(dp[bag]) # n=0的决策结束后,dp表中存储的即为给定条件下的最大价值。
时间优化版
import numpy as np
dp = np.zeros((bag+1)) # 物品0base计数,下标n表示所有决策进行后的边界。
dp[rest] = 0 # 决策边界之外任何剩余空间都无法带来价值
for n in range(N-1, -1, -1):
wn = w[n]
vn = v[n]
for rest in range(bag, wn-1, -1):
if dp[rest]<(dp[rest-wn]+vn):
dp[rest] = dp[rest-wn]+vn
print(dp[bag]) # n=0的决策结束后,dp表中存储的即为给定条件下的最大价值。
网上多数的背包递推方程为:
f ( n , r e s t ) = { f ( n − 1 , rest ) , rest < w ( n ) ; max { f ( n − 1 , rest ) , f ( n − 1 , rest − w ( n ) ) + v ( n ) } , rest ≥ w ( n ) . \begin{equation*} f(n, rest)= \begin{cases} f(n-1,\text{rest}),&\text{rest}<w(n);\\ \max \{f(n-1,\text{rest}), f(n-1,\text{rest}-w(n))+v(n)\},&\text{rest} \ge w(n). \end{cases} \end{equation*} f(n,rest)={f(n−1,rest),max{f(n−1,rest),f(n−1,rest−w(n))+v(n)},rest<w(n);rest≥w(n).
他们的递推边界是f(0,rest),物品1base计数。以从边界向另一个方向推进的思想可以看出这两种方法是等价的。
区别在于可解释性中的决策方向。
假设物品摆放成一行后自左向右以0开始到N的编号。
我的n是自左向右决策时对第n件物品的决策。
传统公式可以看作自右向左进行的决策,这样回归时就是自左向右了,这时边界只能用0存储,因此他们使用1base。
蛮力递归 36 / 143 个通过的测试用例
class Solution {
public:
vector<int> nums;
bool recursive(int index, int bag){
if (bag<0) return false;
if(index>=nums.size()){
return bag==0;
}
bool plan0 = recursive(index+1, bag);
bool plan1 = recursive(index+1, bag-nums[index]);
return plan0 || plan1;
}
bool canPartition(vector<int>& nums) {
this->nums=nums;
int sum=0;
for(const int & num:nums){
sum += num;
}
if(sum%2) return false;
bool res = recursive(0, sum/2);
return res;
}
};
二维DP,且内层循环的边界不够优。 T ( n ) = O ( n ⋅ 1 2 ∑ i nums i ) T(n)=O(n \cdot \frac{1}{2} \sum_i \text{nums}_i) T(n)=O(n⋅21∑inumsi)(327ms)
bool canPartition(vector<int>& nums) {
int sum=0, n=nums.size();
sum = accumulate(nums.begin(), nums.end(), 0);
if(sum%2) return false;
int bag = sum/2;
vector<vector<bool>> memo(nums.size()+1, vector<bool>(bag+1,false));
memo[n][0]=true;
for(int index=n-1; index>=0; --index){
for(int rest=0; rest<=bag; ++rest){
int cur_weight = nums[index];
if(rest-cur_weight<0){
memo[index][rest]=memo[index+1][rest];
}else{
memo[index][rest]=memo[index+1][rest]||memo[index+1][rest-cur_weight];
}
}
}
return memo[0][bag];
}
一维DP,最终优化版 T ( n ) = O ( ∑ j ( 1 2 ∑ i nums i − nums j ) ) = O ( ( n − 1 ) ⋅ 1 2 ∑ i nums i ) T(n)=O(\sum_j(\frac{1}{2} \sum_i \text{nums}_i-\text{nums}_j))=O((n-1) \cdot \frac{1}{2} \sum_i \text{nums}_i) T(n)=O(∑j(21∑inumsi−numsj))=O((n−1)⋅21∑inumsi),运行时间更快(35ms),包括内层循环优化与节省内存分配时间。二维版没办法做内循环优化,因为他需要拷贝 n + 1 n+1 n+1次决策的结果来覆盖初始值。但是他的优点是可以通过回溯获取对应的解。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
int bag = accumulate(nums.begin(), nums.end(), 0);
if(bag%2) return false;
bag /= 2;
vector<int> dp(bag+1, 0);
dp[0]=1;
for(int i = n-1; i >= 0; --i){
int wn = nums[i];
for(int rest=bag; rest>=wn; --rest){
if(dp[rest-wn]||dp[rest]){
dp[rest]=1;
}
}
}
return dp[bag];
}
};
多重背包
行展开成01背包即可
完全背包
每种物品的数量不设上限。
LeetCode279完全平方数
朴素三重循环
超时 570 / 589 个通过的测试用例
class Solution {
public:
int numSquares(int n) {
int i = 1;
vector<int> squares;
while(i*i<=n){
squares.push_back(i*i);
++i;
}
vector<vector<int>>dp(squares.size()+1, vector<int>(n+1, INT_MAX));
dp[squares.size()][0]=0;
for(i=squares.size()-1; i >= 0; --i){
int wn=squares[i];
for(int rest = 0; rest<=n; ++rest){
dp[i][rest]=dp[i+1][rest];
for(int k = 1; k*wn<=rest; ++k){
if(dp[i+1][rest-k*wn]!=INT_MAX&&dp[i+1][rest-k*wn]+k<dp[i][rest]){
dp[i][rest]=dp[i+1][rest-k*wn]+k;
}
}
}
}
return dp[0][n];
}
};
两重循环版
从以上代码中可以看到, rest − k ∗ w n < rest, k > 0 \text{rest}-k*wn<\text{rest},k>0 rest−k∗wn<rest,k>0。也就是当rest循环是从小到大时, d p [ i ] [ rest − w n ] dp[i][\text{rest}-wn] dp[i][rest−wn]其实已经更新完了,而且他自己需要检查所有 k k k更大的情况,并将结果保存到自己的位置,因此循环控制 k k k可以解除。
即 d p [ i ] [ rest − w n ] dp[i][\text{rest}-wn] dp[i][rest−wn]在更新的时候检查了
d p [ i + 1 ] [ rest − w n − k ⋅ w n ] + k , k ≥ 1 : = d p [ i + 1 ] [ rest − k ⋅ w n ] + k − 1 , k ≥ 0 dp[i+1][\text{rest}-wn-k \cdot wn]+k,k \ge 1:=dp[i+1][\text{rest}-k \cdot wn]+k-1, k \ge 0 dp[i+1][rest−wn−k⋅wn]+k,k≥1:=dp[i+1][rest−k⋅wn]+k−1,k≥0
并将其中最小值存储在自己的位置。而 d p [ i ] [ rest ] dp[i][\text{rest}] dp[i][rest]需要检查的
d p [ i + 1 ] [ rest − k ⋅ w n ] + k , k ≥ 0 dp[i+1][\text{rest}-k \cdot wn]+k, k \ge 0 dp[i+1][rest−k⋅wn]+k,k≥0,
除了 k = 0 k=0 k=0都已经减1比较过并存放在 d p [ i ] [ rest − w n ] dp[i][\text{rest}-wn] dp[i][rest−wn],因此只需要将 d p [ i ] [ rest − w n ] + 1 dp[i][\text{rest}-wn]+1 dp[i][rest−wn]+1即可获得至少选择1项时的最优解。
class Solution {
public:
int numSquares(int n) {
int i = 1;
vector<int> squares;
while(i*i<=n){
squares.push_back(i*i);
++i;
}
vector<vector<int>>dp(squares.size()+1, vector<int>(n+1, INT_MAX));
dp[squares.size()][0]=0;
for(i=squares.size()-1; i >= 0; --i){
int wn=squares[i];
for(int rest = 0; rest<=n; ++rest){
dp[i][rest]=dp[i+1][rest];
if(rest>=wn&&dp[i][rest-wn]!=INT_MAX&&dp[i][rest-wn]+1<dp[i][rest]){
dp[i][rest]=dp[i][rest-wn]+1;
}
}
}
return dp[0][n];
}
};
滚动DP版
class Solution {
public:
int numSquares(int n) {
int i = 1;
vector<int> squares;
while(i*i<=n){
squares.push_back(i*i);
++i;
}
vector<int>dp(n+1, INT_MAX);
dp[0]=0;
for(i=squares.size()-1; i >= 0; --i){
int wn=squares[i];
for(int rest = wn; rest<=n; ++rest){
if(dp[rest-wn]!=INT_MAX&&dp[rest-wn]+1<dp[rest]){
dp[rest]=dp[rest-wn]+1;
}
}
}
return dp[n];
}
};
LeetCode322零钱兑换
朴素三重循环+回溯法获取优化解
超时 115 / 189 个通过的测试用例
本题超时原因是回溯的复杂度而非三重循环。在满足题意的解足够多时,从回溯中获取最小硬币数的方法就会超时。
追踪数组记录递归树可以保存一个可行解,但是对解的额外约束无法满足。
因此本题需要直接以问题的目标作为优化函数而不是作为完全背包问题间接获取。
class Solution {
public:
vector<vector<int>> dp;
int n_coins=INT_MAX, n;
int coinChange(vector<int>& coins, int amount) {
n = coins.size();
dp = vector<vector<int>>(n+1, vector<int>(amount+1, 0));
for(int i = n-1; i>=0; --i){
int wn=coins[i]; int vn=coins[i];
for(int rest = 0; rest<=amount; ++rest){
dp[i][rest]=dp[i+1][rest];
for(int k = 1; k*wn<=rest; ++k){
if (dp[i][rest]<dp[i+1][rest-k*wn]+k*vn){
dp[i][rest]=dp[i+1][rest-k*wn]+k*vn;
}
}
}
}
if(dp[0][amount]!=amount){
return -1;
}
backtrace(coins, 0, 0, amount, amount);
return n_coins;
}
inline void backtrace(const vector<int>& coins, int i, int n_coins, int bag, int total){
if(total==0 && n_coins<this->n_coins){
this->n_coins = n_coins;
return;
}
if(i==n || bag<0){
return;
}
int wn=coins[i], vn=coins[i];
for(int k = 0; k*wn<=bag; k++){
if(dp[i+1][bag-k*wn]+k*vn==total){
backtrace(coins, i+1, n_coins+k, bag-k*wn, total-k*vn);
}
}
}
};
朴素三重循环+标记法获取优化解
由于优化目标此时是恰好装满,拿到的任意可行解并不满足用币最少的要求。此算法只展示如何通过标记数组拿到一个解的流程。
class Solution {
public:
vector<vector<int>> dp;
vector<vector<int>> mark;
int n_coins=INT_MAX, n;
int coinChange(vector<int>& coins, int amount) {
sort(coins.begin(), coins.end(), less<int>());
n = coins.size();
dp = vector<vector<int>>(n+1, vector<int>(amount+1, 0));
mark = vector<vector<int>>(n+1, vector<int>(amount+1,INT_MAX));
for(int rest = coins[n-1]; rest<=amount; ++rest){
mark[n-1][rest] = n-1;
}
for(int i = n-1; i>=0; --i){
int wn=coins[i]; int vn=coins[i];
for(int rest = 0; rest<=amount; ++rest){
dp[i][rest]=dp[i+1][rest];
mark[i][rest]=mark[i+1][rest];
for(int k = 1; k*wn<=rest; ++k){
if (dp[i][rest]<dp[i+1][rest-k*wn]+k*vn){
dp[i][rest]=dp[i+1][rest-k*wn]+k*vn;
mark[i][rest]=i;
}
}
}
}
if(!amount) return 0;
if(dp[0][amount]!=amount){
return -1;
}
n_coins = get_solution(mark, amount, coins);
return n_coins;
}
int get_solution(vector<vector<int>> &mark, int bag, vector<int>& coins){
vector<int>selected(mark.size(), 0);
for(int i = mark.size()-1; i>=0; --i){
int wi = coins[i];
while(mark[i][bag]==i){
selected[i]++;
bag-=wi;
}
}
int res = accumulate(selected.begin(), selected.end(), 0);
return res;
}
};
记忆化搜索
对于完全背包问题,也可采取记忆化搜索逐个选取的方案求解。区别在于无法确定循环次数。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
// 丢掉面值过大的硬币在某些情况下可以优化 但是本题样例没有明显提升
// sort(coins.begin(), coins.end());
// auto it = upper_bound(coins.begin(), coins.end(), amount);
// vector<int>coins_cp=coins;
// if(it != coins.end()){
// int num_candidate = distance(coins.begin(), it);
// coins_cp = vector<int>(num_candidate, 0);
// copy(coins.begin(), it, coins_cp.begin());
// }
// coins = coins_cp;
vector<int> dp(amount+1, INT_MAX);
dp[0]=0;
for (const int &coin:coins){
if(coin>=dp.size()) continue;
dp[coin]=1;
}
while(true){
bool better=false;
for(int rest = amount; rest>=0; --rest){
for (const int &coin:coins){
if(rest>coin&&dp[rest-coin]!=INT_MAX && dp[rest]>dp[rest-coin]+1){
better=true;
dp[rest] = dp[rest-coin]+1;
}
}
}
if(!better || dp[amount]!=INT_MAX) break;
}
if(dp[amount]==INT_MAX){
return -1;
}else{
return dp[amount];
}
}
};
DP版
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1, INT_MAX);
dp[0]=0;
for (const int &coin:coins){
if(coin>=dp.size()) continue;
dp[coin]=1;
}
for(int i = coins.size()-1; i>=0; --i){
int wn = coins[i];
for(int rest=wn; rest<=amount; ++rest){
if(dp[rest-wn]!=INT_MAX && dp[rest-wn]+1<dp[rest]){
dp[rest]=dp[rest-wn]+1;
}
}
}
if(dp[amount]==INT_MAX){
return -1;
}else{
return dp[amount];
}
}
};
从组合解到排列解
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned int> dp(target+1, 0);
dp[0]=1;
for(int rest=0; rest<=target; ++rest){
for(int i = nums.size()-1; i>=0; --i){
int val_i = nums[i];
if(rest>=val_i){
dp[rest]+=dp[rest-val_i];
}
}
}
return dp[target];
}
};
最大连续子列和问题
最长回文子串
最长公共子序列(LCS)
最长不下降子序列(LIS)
最长严格递增子序列长度
存在 O ( n 2 ) O(n^2) O(n2)与 O ( n log ( n ) ) O(n\log(n)) O(nlog(n))两种做法。
O
(
n
2
)
O(n^2)
O(n2)的思想就是找到
i
i
i元素前面更小且最长的元素
j
j
j,把那个元素的长度加一即可。
O
(
n
2
)
O(n^2)
O(n2)算法实现
class Solution {
public:
vector<int> memo;
int get_rank(int rank_pre, vector<int>& nums){
int i = rank_pre + 1;
int res = -1;
int length_max = 0;
while(rank_pre >= 0 ){
if (nums[rank_pre] < nums[i]){
if (memo[rank_pre] > length_max){
res = rank_pre;
length_max = memo[rank_pre];
}
}
rank_pre--;
}
return res;
}
int lengthOfLIS(vector<int>& nums) {
memo = vector<int>(nums.size(), 0);
memo[0] = 1;
for (int i = 1; i < nums.size(); i++){
int rank_pre = get_rank(i-1, nums);
if (rank_pre >= 0){
memo[i] = memo[rank_pre] + 1;
}else{
memo[i] = 1;
}
}
int res = 1;
for (int & length:memo){
res = length > res ? length : res;
}
return res;
}
};
动机:如果总能知道元素
e
i
e_i
ei之前的最长严格单调递增序列的最后一个元素
e
j
e_j
ej,那么我们就能根据
i
,
j
i,j
i,j大小判定序列长度是否能进一步增长。从贪心的角度,元素
e
j
e_j
ej尽可能小,序列长度更可能增长。
如何设计一个数据结构存储元素
e
j
e_j
ej?
当我们发现一个元素
e
i
e_i
ei比元素
e
j
e_j
ej小并且比元素
e
j
−
1
e_{j-1}
ej−1大的时候我们找到了更小的
e
j
e_j
ej,替换即可。
似乎我们需要存储的是遍历到此处时找到的最长严格单调递增序列。
但是上述贪心是存在瑕疵的,我们只能替换现有长度的最后一个元素,并保持前
i
−
1
i-1
i−1个元素为子序列前缀。但是最优解不一定在这个子空间中。
最优解的第
i
i
i个元素可能在已经存储了长度
n
>
i
n>i
n>i的第
k
k
k次比较时才出现。
为了保证最优解的第
i
i
i个元素一定被存储。我们真正需要存储的
f
f
f,需要满足
f
[
i
]
f[i]
f[i]是长度
i
+
1
i+1
i+1的已知严格单调递增序列的最后一个且最小的元素。
假设我们已经在第
k
k
k次比较时拥有了这样的结构,元素
e
k
e_k
ek要如何更新
f
f
f?
替换掉
f
f
f中比元素
e
k
e_k
ek大的第一个元素
e
l
e_l
el,不存在则直接附加到末尾。
证明这样做保持性质:
长度 i + 1 i+1 i+1的已知严格单调递增序列的前 i i i个元素加上元素 e k e_k ek还是一个严格单调递增序列。
f
f
f中的元素的更新会导致它可能不再是一个合法的最长严格单调递增序列。
但是它总是合法的序列末尾最小元素存储结构。
O
(
n
log
(
n
)
)
O(n\log(n))
O(nlog(n))算法实现
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> min_lasts;
for (const int &num : nums){
auto it = std::lower_bound(min_lasts.begin(), min_lasts.end(), num);
if (it == min_lasts.end()){
min_lasts.push_back(num);
}else{
*it = num;
}
}
return min_lasts.size();
}
};
俄罗斯套娃问题
这是二维的最长严格递增子序列
O(n^3) 超时 84 / 87 个通过的测试用例
使用排序,unordered_set记录剩余可选信封可以从79/87优化到84/87。
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
sort(envelopes.begin(), envelopes.end(), [](vector<int>&e0, vector<int>&e1)->bool{
if (e0[0]==e1[0]){
return e0[1]<e1[1];
}else{
return e0[0]<e1[0];
}
});
int num = envelopes.size();
vector<vector<int>> memo(num, vector<int>(num, 0));
for(int i = 0; i < num; i++){
memo[0][i]=1;
}
// rank_i enve_j
unordered_set<int> survivors;
for(int i = 0; i < num; i++){
survivors.insert(i);
}
int i=1;
for(; i < num; i++){
bool no_more=true;
for(const int& j : survivors){
for(int k = 0; k < j; k++){
if (memo[i-1][k]&&envelopes[k][0]<envelopes[j][0]&&envelopes[k][1]<envelopes[j][1]){
memo[i][j]=1;
no_more=false;
break;
}
}
}
unordered_set<int> survivors_cp(survivors);
for(const int& j:survivors_cp){
if(!memo[i][j]) survivors.erase(j);
}
if(no_more){
break;
}
}
return i;
}
};
排序 O ( n log ( n ) ) O(n\log(n)) O(nlog(n)),然后利用严格最长递增子序列长度算法 O ( n 2 ) O(n^2) O(n2)或者改进版 O ( n log ( n ) ) O(n\log(n)) O(nlog(n))。
改进版算法
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
sort(envelopes.begin(), envelopes.end(), [](vector<int>&e0, vector<int>&e1)->bool{
if (e0[0]==e1[0]){
return e0[1]>e1[1];
}else{
return e0[0]<e1[0];
}
});
int num = envelopes.size();
vector<int> min_last;
for(const auto &enve:envelopes){
int height = enve[1];
auto it = lower_bound(min_last.begin(), min_last.end(), height);
if(it == min_last.end()){
min_last.push_back(height);
}else{
*it = height;
}
}
return min_last.size();
}
};
三维实心物体堆叠最高高度问题
O ( n 3 ) O(n^3) O(n3)的算法可以解决求三维物体实心堆叠最大高度的问题。要求每个维度都小才能堆叠。
核心算法
int maxGifts(vector<vector<int>>& gifts) {
sort(gifts.begin(), gifts.end());
int num = gifts.size();
vector<vector<int>> memo(num, vector<int>(num, 0));
int max_height=0;
for(int i = 0; i < num; i++){
memo[0][i]=gifts[i][2];
max_height = memo[0][i]>max_height?memo[0][i]:max_height;
}
// rank_i enve_j
unordered_set<int> survivors;
for(int i = 0; i < num; i++){
survivors.insert(i);
}
for(int i=1; i < num; i++){
bool no_more=true;
for(const int& j : survivors){
for(int k = 0; k < j; k++){
if (memo[i-1][k]&&
gifts[k][0]<gifts[j][0]&&
gifts[k][1]<gifts[j][1]&&
gifts[k][2]<gifts[j][2]){
if(memo[i][j]<memo[i-1][k]+gifts[j][2]){
memo[i][j]=memo[i-1][k]+gifts[j][2];
no_more=false;
max_height = max_height<memo[i][j]?memo[i][j]:max_height;
}
}
}
}
unordered_set<int> survivors_cp(survivors);
for(const int& j:survivors_cp){
if(!memo[i][j]) survivors.erase(j);
}
if(no_more){
break;
}
}
return max_height;
}
完整算法
#include<bits/stdc++.h>
using namespace std;
namespace std{
template <>
struct less<vector<int>> {
bool operator()(const vector<int>& e0, const vector<int>& e1) const {
if(e0[0]<e1[0]){
return true;
}else if(e0[0]>e1[0]){
return false;
}else{
if(e0[1]<e1[1]){
return true;
}else if(e0[1]>e1[1]){
return false;
}else{
return e0[2]<e1[2];
}
}
}
};
}
class Solution {
public:
int maxGifts(vector<vector<int>>& gifts) {
sort(gifts.begin(), gifts.end());
int num = gifts.size();
vector<vector<int>> memo(num, vector<int>(num, 0));
int max_height=0;
for(int i = 0; i < num; i++){
memo[0][i]=gifts[i][2];
max_height = memo[0][i]>max_height?memo[0][i]:max_height;
}
// rank_i enve_j
unordered_set<int> survivors;
for(int i = 0; i < num; i++){
survivors.insert(i);
}
for(int i=1; i < num; i++){
bool no_more=true;
for(const int& j : survivors){
for(int k = 0; k < j; k++){
if (memo[i-1][k]&&
gifts[k][0]<gifts[j][0]&&
gifts[k][1]<gifts[j][1]&&
gifts[k][2]<gifts[j][2]){
if(memo[i][j]<memo[i-1][k]+gifts[j][2]){
memo[i][j]=memo[i-1][k]+gifts[j][2];
no_more=false;
max_height = max_height<memo[i][j]?memo[i][j]:max_height;
}
}
}
}
unordered_set<int> survivors_cp(survivors);
for(const int& j:survivors_cp){
if(!memo[i][j]) survivors.erase(j);
}
if(no_more){
break;
}
}
return max_height;
}
};
int main(){
string line;
std::getline(cin, line);
istringstream iss(line);
int n;
iss >> n;
vector<vector<int>> gifts(n,vector<int>(3,0));
for(int i = 0; i < n; i++){
std::getline(cin, line);
istringstream iss(line);
iss >> gifts[i][0] >> gifts[i][1] >> gifts[i][2];
}
Solution s;
auto res = s.maxGifts(gifts);
cout << res;
return 0;
}
区间DP
基站盈利问题
Memory limit exceeded 10/15 accepted
#include<bits/stdc++.h>
using namespace std;
int n, m;
int main(){
vector<vector<int>> memo;
string line;
std::getline(cin, line, '\n');
istringstream iss(line);
iss >> n;
iss.clear();
std::getline(cin, line, '\n');
iss.str(line);
iss >> m;
iss.clear();
memo = vector<vector<int>>(n+1, vector<int>(n+1, 0));
for(int i = 0; i < m; i++){
std::getline(cin, line, '\n');
iss.str(line);
int s_a, s_b, p;
iss >> s_a >> s_b >> p;
if(memo[s_a][s_b]<p){
memo[s_a][s_b]=p;
}
iss.clear();
}
for (int length=1; length<=n; length++){
for(int start=1; start+length-1<=n; start++){
int end=start+length-1;
for(int mid=start; mid<end; mid++){
if(memo[start][mid]+memo[mid+1][end]>memo[start][end]){
memo[start][end]=memo[start][mid]+memo[mid+1][end];
}
}
}
}
cout << memo[1][n];
return 0;
}