算法作业——5.6

算法作业——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 value2m ,那么只要在此限制下尽量多得到魔卡。

那么这个问题即变为了一个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{fi1,j,fi1,jwi+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 i1 层,则每次更新覆盖即可,为了防止先更新了较小的 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{fi1,j1,0pi}fi,j,0=max{fi1,j,1+pi}fi,j,0/1=maxfi1,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 i1 层,则每次更新覆盖即可。为了防止先更新了较小的 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+1gkgkgk1 。同时,如果我们 对物品的选择数量没有限制,即 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+1gkgkgk1
是成立的。我们可以这样想:我们每额外增加一笔交易 g k → g k + 1 g_k \rightarrow g_{k+1} gkgk+1 ,那么这一笔交易一定不会比上一笔交易 g k − 1 → g k g_{k-1} \rightarrow g_{k} gk1gk 产生的收益高,否则我们就可以交换这两笔交易,使得 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 的值。

image-20240507192515080

如果我们选择了一个合适的斜率 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 gkkc ,这个价值就是含手续费为 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 gkkc ,要在结果上加上 k ⋅ c k\cdot c kc

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)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值