算法分析与设计 - 作业12
问题一
John 有两个孩子,在 John病逝后,留下了一组价值不一定相同的魔卡, 现在要求你设计一种策略,帮John的经管人将John的这些遗产分给他的两个孩子,使得他们获得的遗产差异最小(每张魔卡不能分拆)。
解法一
原题目可以被更形式化地表示为:给定 \(n\) 个数 \(a_1\sim a_n\),要求给出一组划分,将其划分为两个集合,使得两集合中元素之和的差尽可能的小。考虑到两个集合互为补集,一个显然的想法是考虑使用 01 背包算法求出某个集合的元素之和 \(s\) 的所有可能取值集合 \(S\),确定了其中一个集合的元素之和,则另一集合中元素之和即可直接得到,则答案即为:
时间复杂度为 \(O\left(n\times \sum {a_i}\right)\) 级别。
#include<bits/stdc++.h>
#define ffor(i,a,b) for(int i=a;i<=b;++i)
#define rfor(i,a,b) for(int i=a;i>=b;--i)
using namespace std;
const int N=1e3+5;
const int M=1e5+5;
int dp[M],ar[N],n,sum;
signed main(){
cin>>n;
ffor(i,1,n)cin>>ar[i],sum+=ar[i];
dp[0]=1;
ffor(i,1,n)
rfor(j,sum,ar[i])
dp[j]|=dp[j-ar[i]];
int ans=sum;
ffor(i,1,sum)
if(dp[i])
ans=min(ans,abs(i+i-sum));
cout<<ans;
return 0;
}
解法二
发现对于每一种划分方案,总有某个集合中的元素个数不大于 \(\frac{n}{2}\)。
于是考虑折半搜索,转化为枚举元素数量较少的集合中元素之和的所有可能取值,并检查最小差值即可。
总时间复杂度 \(O(2^{\frac{n}{2}})\) 级别,与值域无关。
解法三(?
通过观察可得结论:划分后得到的两集合中元素之和的差值的最小值,一定不大于 \(\max a_i\)。
对于上述结论,可以证明若对于不满足上述条件的划分,可以通过调整划分使之满足上述条件后使差值更小。设划分得到的两个互补集合分别为 \(S_1, S_2\),且 \(d = \left|\sum_{s\in S_1} s - \sum_{s\in S_2} s\right| > \max a_i\)。考虑设 \(\sum_{s\in S_1} s \ge \sum_{s\in S_2} s\):
- 若 \(\max a_i \in S_2\),考虑从 \(S_1\) 中选择某些元素将它们划分到 \(S_2\) 中,从而减少两个集合元素之和的差值。
- 仅需在保持 \(\sum_{s\in S_1} s \ge \sum_{s\in S_2} s\) 的情况下,不断地选择 \(S_1\) 中的最小值划分到 \(S_2\) 中即可。
- 当无法再选择 \(S_1\) 中的最小值划分到 \(S_2\) 中,且保持 \(\sum_{s\in S_1} s \ge \sum_{s\in S_2} s\) 时,说明此时 \(d < \min\{ S_1\} \le \max a_i\),满足上述结论。
- 若 \(\max a_i \in S_1\),考虑从 \(S_1\) 中将 \(\max a_i\) 划分到 \(S_2\) 中,则此时 \(d\) 一定减少了 \(\max a_i\),仍保持 \(\sum_{s\in S_1} s \ge \sum_{s\in S_2} s\),且新划分一定比原划分方案更优。
- 若此时仍有 \(d > \max a_i\),转化为 \(\max a_i \in S_2\) 的情况,按照上述方案进行调整即可。
- 否则调整到了 \(d \le \max a_i\),符合上述结论。
然而这个启发式的结论没什么用哈哈,只能用来特判进行常数优化。
解法四(?
明明感觉这问题很简单可以随手写出一堆求近似解的启发式解法,但是对精确解束手无策了。
查阅资料发现本问题为经典的 NP-hardness 的分区问题:Partition problem - Wikipedia,现阶段难以找到多项式复杂度的解法。
不玩了!投降!
问题二
假设已知某股票连续若干天的股价,并且如何时候你手上只能由一支股票,即如果你要买入就得先将手上股票卖出,设计一个算法来计算你所能获取的最大利润。你最多可以完成 k笔交易。也就是说,你最多可以买k 次,卖 k 次。
设一共有 \(n\) 天,第 \(i\) 天的股票价格为 \(a_i\)。
发现在某天是否可以买卖股票仅需考虑此时手中是否有股票即可,与之前的操作无关,每天的状态可以用此时进行了多少次买卖与手中是否有股票唯一表示,于是考虑动态规划。
显然最优情况下,结束 \(n\) 天后手中一定不会有股票,于是记 \(f_{i, j, 0 \text{ or } 1}\) 表示在第 \(i\) 天结束时,已经进行了 \(j\) 次卖出操作,此时手中没有/有股票时,可以获得的最大利润总和。初始化 \(\forall 0\le i\le n, 0\le j\le k, 0\le l\le 1,\ f_{i, j, l} = -\infin\),\(f_{0, 0, 0} = 0\),考虑枚举每天并考虑当天是否进行交易,则有状态转移方程:
若不进行交易,则有:
若进行买入操作,则有:
进行卖出操作,则有:
最终答案即为:
状态空间复杂度 \(O(nk)\) 级别,转移时间复杂度 \(O(nk)\) 级别,总时空复杂度均为 \(O(nk)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 3e3 + 10;
const int kK = 3e3 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, k, a[kN];
LL f[kN][kK][2];
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> k;
for (int i = 1; i <= n; ++ i) std::cin >> a[i];
for (int i = 0; i <= n; ++ i) {
for (int j = 0; j <= k; ++ j) {
f[i][j][0] = f[i][j][1] = -kInf;
}
}
f[0][0][0] = 0;
for (int i = 1; i <= n; ++ i) {
for (int j = 0; j <= k; ++ j) {
f[i][j][0] = f[i - 1][j][0];
f[i][j][1] = std::max(f[i - 1][j][1], f[i - 1][j][0] - a[i]);
if (j > 0) {
f[i][j][0] = std::max(f[i][j][0], f[i - 1][j - 1][1] + a[i]);
}
}
}
LL ans = -kInf;
for (int j = 0; j <= k; ++ j) ans = std::max(ans, f[n][j][0]);
std::cout << ans << "\n";
return 0;
}
/*
6 2
3 2 7 5 1 4
*/
写在最后
参考: