@description@
给定两个长度为 n 的正整数序列 {ai} 与 {bi},序列的下标为 1,2,…,n。
现在你需要分别对两个序列各指定恰好 K 个下标,要求至少有 L 个下标在两个序列中都被指定,使得这 2K 个下标在序列中对应的元素的总和最大。
形式化地说,你需要确定两个长度为 K 的序列 {ci},{di},其中 1≤c1<c2<…<cK≤n,1≤d1<d2<…<dK≤n,并要求 |{c1,c2,…,cK}∩{d1,d2,…,dK}|≥L。目标是最大化:
@solution@
首先 O(n^3) 的 dp 人人都会。然后据说可以优化出来。
考虑换思路,使用费用流建模:
将点 i 拆成 i -> i',费用 0 容量 1;
然后连 s1 -> i 费用 ai 容量 1,连 i' -> t1 费用 bi 容量 1。
再连 s2 -> i' 费用 0 容量 1,连 i -> t2 费用 0 容量 1。
最后连 s -> s1 费用 0 容量 K,连 s -> s2 费用 0 容量 K - L;连 t1 -> t 费用 0 容量 K,连 t2 -> t 费用 0 容量 K - L。
(好像跟网上的一般建图方法不大一样来着。。。不过无所谓,一样可以优化)
这样跑最小费用流(或者最小费用最大流也可)就可以有 40 分的好成绩(靠,我当时为什么没想到)。
考虑优化。注意使用连续最短路算法时,我们总会先选择前 K 个 ai + bi 最大的 i,并发送流量 s -> s1 -> i -> i' -> t1 -> t。
然后我们会跑 K - L 次最短路。每次最短路总会是以下几种效果之一:
1)ai + bi -> ai + bj。
2)ai + bi -> aj + bi。
3)ai + bi + aj + bj -> ai + bj + ak + bk。
4)ai + bi -> aj + bk。
可以自己手算一下边界情况(如 K - L = 1)验证一下,或者手画一个网络流跑一跑。
然后就可以用可删除堆 O(nlog n) 乱做。
@accepted code@
#include <queue>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, int> pli;
#define fi first
#define se second
#define mp make_pair
const int MAXN = 200000;
const ll INF = (1LL << 60);
int tag[MAXN + 5];
struct heap{
int type; priority_queue<pli>q; heap() {}
heap(int _t) : type(_t) {}
void maintain() {
while( !q.empty() && tag[q.top().se] != type && (type == 3 || tag[q.top().se] != 0) )
q.pop();
}
bool empty() {maintain(); return q.empty();}
pli top() {maintain(); return empty() ? make_pair(-INF, -1) : q.top();}
void push(pli x) {q.push(x); maintain();}
void pop() {q.pop(); maintain();}
};
int read() {
int x = 0, ch = getchar();
while( ch > '9' || ch < '0' ) ch = getchar();
while( '0' <= ch && ch <= '9' ) x = 10*x + ch - '0', ch = getchar();
return x;
}
pii a[MAXN + 5];
bool cmp_ab(const pii &x, const pii &y) {
return x.fi + x.se > y.fi + y.se;
}
void solve() {
int n = read(), K = read(), L = read();
for(int i=1;i<=n;i++) a[i].fi = read();
for(int i=1;i<=n;i++) a[i].se = read();
ll ans = 0; heap hn1(3), hn2(3), hn3(3), hx1(0), hx2(1), hx3(2);
sort(a + 1, a + n + 1, cmp_ab);
for(int i=1;i<=K;i++) {
ans += a[i].fi + a[i].se;
tag[i] = 3;
hn1.push(mp(-(a[i].fi + a[i].se), i));
hn2.push(mp(-a[i].fi, i)), hn3.push(mp(-a[i].se, i));
}
for(int i=K+1;i<=n;i++) {
tag[i] = 0;
hx1.push(mp(a[i].fi + a[i].se, i));
hx2.push(mp(a[i].fi, i)), hx3.push(mp(a[i].se, i));
}
for(int i=1;i<=K-L;i++) {
pli mnab = hn1.top(), mna = hn2.top(), mnb = hn3.top();
pli mxab = hx1.top(), mxa = hx2.top(), mxb = hx3.top();
ll da = mxa.fi + mna.fi, db = mxb.fi + mnb.fi;
ll dc = mxab.fi + mna.fi + mnb.fi, dd = mxa.fi + mxb.fi + mnab.fi;
if( dd > dc && dd > db && dd > da && dd > 0 ) {
ans += dd;
tag[mxb.se] = 1, hx2.push(mp(a[mxb.se].fi, mxb.se));
tag[mxa.se] = 2, hx3.push(mp(a[mxa.se].se, mxa.se));
tag[mnab.se] = 0;
hx1.push(mp(a[mnab.se].fi + a[mnab.se].se, mnab.se));
hx2.push(mp(a[mnab.se].fi, mnab.se)), hx3.push(mp(a[mnab.se].se, mnab.se));
} else if( dc > da && dc > db && dc > 0 ) {
ans += dc;
tag[mna.se] = 1, hx2.push(mp(a[mna.se].fi, mna.se));
tag[mnb.se] = 2, hx3.push(mp(a[mnb.se].se, mxb.se));
tag[mxab.se] = 3;
hn1.push(mp(-(a[mxab.se].fi + a[mxab.se].se), mxab.se));
hn2.push(mp(-a[mxab.se].fi, mxab.se)), hn3.push(mp(-a[mxab.se].se, mxab.se));
} else if( da > db && da > 0 ) {
ans += da;
tag[mna.se] = 1, hx2.push(mp(a[mna.se].fi, mna.se));
tag[mxa.se] = 2, hx3.push(mp(a[mxa.se].se, mxa.se));
} else if( db > 0 ) {
ans += db;
tag[mxb.se] = 1, hx2.push(mp(a[mxb.se].fi, mxb.se));
tag[mnb.se] = 2, hx3.push(mp(a[mnb.se].se, mnb.se));
} else break;
}
printf("%lld
", ans);
}
int main() {
freopen("sequence.in", "r", stdin);
freopen("sequence.out", "w", stdout);
int T; scanf("%d", &T);
while( T-- ) solve();
}
@details@
去年写了一个丢人的假贪心,还没发现连大样例都过不了 = =。
关于模拟费用流,除了直接优化连续最短路算法过程外,还有就是 WC2019 上讲的一类老鼠进洞问题(每次加边,动态维护费用流)。
这种用费用流的思想分析贪心问题(或者用贪心优化费用流过程)的好处是,它原本构造性的结论可以与费用流中众所周知的性质转化,更方便思考。