算法作业——5.6
问题一
John 有两个孩子,在 John病逝后,留下了一组价值不一定相同的魔卡, 现在要求你设计一种策略,帮John的经管人将John的这些遗产分给他的两个孩子,使得他们获得的遗产差异最小(每张魔卡不能分拆)。
解决思路
解法一
暴力枚举。总共 n n n 个魔卡,那么我们算出每一种选择下两个孩子的遗产差异即可。共 2 n 2^n 2n 种情况, 可利用dfs完成搜索并更新答案。
时间复杂度 O ( 2 n ) O(2^n) O(2n)
解法二
假设魔卡总价值为 m m m ,那么当遗产分配完只有两种情况:1.两个孩子正好各 m 2 \frac m2 2m 2.一个孩子大于 m 2 \frac m2 2m ,另一个则小于。假设第一个孩子得到的遗产总 v a l u e ≤ m 2 value\leq \frac m2 value≤2m ,那么只要在此限制下尽量多得到魔卡。
那么这个问题即变为了一个01背包问题,且每个物品的价值和体积均为魔卡的价值。设
f
i
,
j
f_{i,j}
fi,j 表示在前
i
i
i 件物品中选择,背包容量为
j
j
j 时可获得的最大价值,则转移方程为
f
i
,
j
=
max
{
f
i
−
1
,
j
,
f
i
−
1
,
j
−
w
i
+
w
i
}
f_{i,j}=\max\{f_{i-1,j},f_{i-1,j-w_i}+w_i\}
fi,j=max{fi−1,j,fi−1,j−wi+wi}
void solu{
for(int i=1;i<=n;i++)
for(int j=m/2; j>=w[i];j--){
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+w[i])
num[j]=i; // 记录对应选定的魔卡序号
}
}
背包问题也可以对空间复杂度进行优化,即使用滚动数组。原理为每次转移只用到了第 i i i 和 i − 1 i-1 i−1 层,则每次更新覆盖即可,为了防止先更新了较小的 j j j ,影响后面的转移,所以 j j j 从倒序转移。
void solu{
for(int i=1;i<=n;i++)
for(int j=m/2; j>=w[i];j--)
if(f[i-1][j]<f[i-1][j-w[i]]+w[i]){
f[i][j]=f[j-w[i]]+w[i];
num[j]=i; // 记录对应选定的魔卡序号
}
}
问题二
假设已知某股票连续若干天的股价,并且如何时候你手上只能由一支股票,即如果你要买入就得先将手上股票卖出,设计一个算法来计算你所能获取的最大利润。你最多可以完成 k笔交易。也就是说,你最多可以买k 次,卖 k 次。
解决思路
解法一
问题是最优问题,但不满足最优子问题,考虑动态规划。
设 f i , j , 0 / 1 f_{i,j,0/1} fi,j,0/1 表示到第 i i i 天,已经买入了 j j j 次,手中有 0 / 1 0/1 0/1 支股票时的最大收益,则分三种情况:1.没有股票这一天买入 2.有股票这一天卖出 3.啥也不干
得到转移方程
f
i
,
j
,
1
=
max
{
f
i
−
1
,
j
−
1
,
0
−
p
i
}
f
i
,
j
,
0
=
max
{
f
i
−
1
,
j
,
1
+
p
i
}
f
i
,
j
,
0
/
1
=
max
f
i
−
1
,
j
,
0
/
1
f_{i,j,1}=\max\{f_{i-1,j-1,0}-p_i\}\\ f_{i,j,0}=\max\{f_{i-1,j,1}+p_i\}\\ f_{i,j,0/1}=\max f_{i-1,j,0/1}
fi,j,1=max{fi−1,j−1,0−pi}fi,j,0=max{fi−1,j,1+pi}fi,j,0/1=maxfi−1,j,0/1
void solu{
for(int i=1;i<=k;i++)
f[1][i][1]=-p[0];
for(int i=2;i<=n;i++){
for(int j=1;j<=k;j++){
if(j==1) f[i][j][1]=max(f[i-1][j][1],-p[i]);
f[i][j][1]=max(f[i-1][j][1],f[i-1][j-1][0]-p[i]);
f[i][j][0]=max(f[i-1][j][0],f[i-1][j][1]+p[i]);
}
}
cout<<f[n][k][0];
}
可以空间优化,同样只用到了第 i i i 和 i − 1 i-1 i−1 层,则每次更新覆盖即可。为了防止先更新了较小的 j j j ,影响后面的转移,所以 j j j 从倒序转移。
void solu{
for(int i=1;i<=k;i++)
f[i][1]=-p[0];
for(int i=2;i<=n;i++){
for(int j=k;j>=1;j--){
if(j==1) f[j][1]=max(f[j][1],-p[i]);
f[j][1]=max(f[j][1],f[j-1][0]-p[i]);
f[j][0]=max(f[j][0],f[j][1]+p[i]);
}
}
cout<<f[n][k][0];
}
时间复杂度为 O ( n k ) O(nk) O(nk) ,空间复杂度为 O ( n ) O(n) O(n)
解法二
wqs二分——给定 n n n 个物品,我们需要在其中 恰好 选择 k k k 个,并且需要最大化收益。设对应的收益为 g k g_k gk ,那么需要满足 在最大化收益的前提下,每多选择一个物品,额外产生的收益是单调递减的,也就是 g k + 1 − g k ≤ g k − g k − 1 g_{k+1}-g_k\leq g_k-g_{k-1} gk+1−gk≤gk−gk−1 。同时,如果我们 对物品的选择数量没有限制,即 k k k 不存在,那么我们应当能够快速地计算出最大的收益,以及达到最大的收益需要选择的物品数量。
我们设恰好完成
k
k
k 笔交易时,能够获取的最大收益为
g
k
g_k
gk ,那么
g
k
+
1
−
g
k
≤
g
k
−
g
k
−
1
g_{k+1}-g_k\leq g_k-g_{k-1}
gk+1−gk≤gk−gk−1
是成立的。我们可以这样想:我们每额外增加一笔交易
g
k
→
g
k
+
1
g_k \rightarrow g_{k+1}
gk→gk+1 ,那么这一笔交易一定不会比上一笔交易
g
k
−
1
→
g
k
g_{k-1} \rightarrow g_{k}
gk−1→gk 产生的收益高,否则我们就可以交换这两笔交易,使得
g
k
g_k
gk 更大,那么就与
g
k
g_k
gk 是恰好完成
k
k
k 笔交易时的最大收益这个事实相矛盾了。
如果我们把 ( k , g k ) (k,g_k) (k,gk) 看成平面直角坐标系上的点,那么这些点就组成了一个上凸壳。
虽然我们并不知道 g k g_k gk 的值到底是多少(否则我们就可以直接返回正确答案了),但是我们知道 ( k , g k ) (k,g_k) (k,gk) 对应的图像的形状,且以 ( k , g k ) (k,g_k) (k,gk) 与 ( k + 1 , g k + 1 ) (k+1,g_{k+1}) (k+1,gk+1) 为端点的线段的 斜率是单调递减的。则可以通过对 斜率进行二分,求出 g k g_k gk 的值。
如果我们选择了一个合适的斜率 c ′ c' c′ ,使得其与上凸壳相切在了某一个我们需要的 ( k , g k ) (k,g_k) (k,gk) 的位置,这样我们就可以在 O ( n ) O(n) O(n) 的时间内直接求出不限制交易次数的最大收益,并且我们知道它实际上就是交易了 k k k 次。
(因为斜率 c c c 且与上凸壳相切的直线的截距为 g k − k ⋅ c g_k-k\cdot c gk−k⋅c ,这个价值就是含手续费为 c c c 的无限制买卖,且恰好交易 k k k 次)
但其实还有两个问题
1.本题中我们限制的是最多进行 k k k 次交易,而不是恰好进行 k k k 次交易
如果 ( k , g k ) (k,g_k) (k,gk) 所在的位置是上凸壳的左半部分(即斜率大于等于 0 0 0 的部分),那么我们就可以使用上面的方法得到答案,这是因为最优的答案一定是进行 k k k 次交易的;
如果 ( k , g k ) (k,g_k) (k,gk) 所在的位置是上凸壳的右半部分(即斜率小于 0 0 0 的部分),那么我们通过二分是没有办法找到斜率 c c c 并且计算出对应的 ( k , g k ) (k,g_k) (k,gk) 的。具体的做法可以在第二个问题中得到解释,也就是我们可以规定二分查找的上下界。
2.二分上下界如何确定
令下界为1,则右半部分会被舍去,若 ( k , g k ) (k,g_k) (k,gk) 的位置在右半部分,则查找失败。
令上界为最大的股价 P P P ,如果查找失败,那么说明最大收益对应的交易次数是严格小于题目中给定的 k k k 的,这就说明 交易次数的限制并不是瓶颈,而价格才是 ,那就可以当作无限交易次数,利用贪心解决
ps:我们二分查找判断的收益是 g k − k ⋅ c g_k-k\cdot c gk−k⋅c ,要在结果上加上 k ⋅ c k\cdot c k⋅c
void solu{
for(int i=1;i<=n;i++)
P=max(P,price[i]);
int l=1,r=P;
int ans=-1;
while(l<=r){
int c=(l+r)>>1;
//无限制的贪心
int buy_cnt=0,sell_cnt=0;
int buy=-price[0],sell=0;
for(int i=1;i<=n;i++){
if(sell-price[i]>=buy){
buy=sell -price[i];
but_cnt=sell_cnt;
}
if(buy+price[i]-c>=sell){
sell=buy+price[i]-c;
sell_cnt=buy_cnt+1;
}
}
if(sell_cnt>=k){
ans=sell+k*c;
l=c+1;
}
else r=c-1;
}
//如果二分查找失败
if(ans==-1){
ans=0;
for(int i=1;i<n;i++)
ans+=max(price[i]-price[i-1],0);
}
cout<<ans;
}
时间复杂度 O ( n log P ) O(n\log P) O(nlogP) ,空间复杂度 O ( 1 ) O(1) O(1)