题目
题目背景
你知道
OUYE
\textsf{OUYE}
OUYE 和
Dd(XyX)
\textsf{Dd(XyX)}
Dd(XyX),三百年前叱咤于大海上的海盗吗?传说他们的宝物正埋在某座海岛下的洞穴中,未曾有人发掘。
没人知道他们留下了什么,唯一可以确定的是:为了找到这些宝物,战火从大陆之东烧到大洋之北,未曾停歇。
寻宝者 OneInDark \textsf{OneInDark} OneInDark 听信群友的指引,来到了一座海岛。海浪不断地翻滚,卷出千层雪。
绕着海岛走了一圈,便看到一个洞口,青草茂盛。走进去,内部空间竟十分宽敞, O n e I n D a r k \sf OneInDark OneInDark 孤零零地站在里面,很显孤独。
走到底,有一石门。上面的凹槽,恰好是一根标杆和一根标枪的形状。下面写着:
神之居所,凡人当止之处。
O n e I n D a r k \sf OneInDark OneInDark 轻推门,门便开了。走进去,只看到一个空空如也的箱子。这箱子,应当原本是装着吸附有 O U Y E \sf OUYE OUYE 神力的坐垫。
旁边是兔子的脚印,和一个蛋的影子,漂浮在空中。
题目描述
传送门 to luogu
思路
一眼线性规划,然后我人傻了 😭
再一搜提交记录,发现 Rainybunny \textsf{Rainybunny} Rainybunny 和 crashed \textsf{crashed} crashed 早就 A A A 了 💢
n a i v e l y \rm naively naively 记 x i , y i x_i,y_i xi,yi 表示是否选择该元素,然后 ∑ min { a i , b i } ⩾ L \sum\min\{a_i,b_i\}\geqslant L ∑min{ai,bi}⩾L 是无法写成网络流的!淦!
必须换一种想法。其实可以先从 反悔贪心 开始想(依据之,很可能建立网络流)。
考虑怎么让 “两边都选” 的多 1 1 1 个。比如直接拼出一个 ( a , b ) (a,b) (a,b),那么 a , b a,b a,b 可能没 “动” 也可能动了:
- ( a ) , ( b ) → ( a , b ) (a),(b)\to(a,b) (a),(b)→(a,b):代价是 Δ a \Delta a Δa 或 Δ b \Delta b Δb 或 Δ a + Δ b \Delta a+\Delta b Δa+Δb 。
或者拆掉一个原有的 ( a , b ) (a,b) (a,b) 再拼出来两个,即
- ( a , b ) , ( a x ) , ( b y ) → ( a x , b ) , ( a , b y ) (a,b),(a_x),(b_y)\to(a_x,b),(a,b_y) (a,b),(ax),(by)→(ax,b),(a,by):代价是 b x + a y − a − b b_x+a_y-a-b bx+ay−a−b 。
注意此时 a x , b y a_x,b_y ax,by 是没有 “动” 的——如果 “动” 了,即在原本 a , b a,b a,b 都没选的位置 ( ∅ ) (\varnothing) (∅) 完成组合。但这种方案等价于先让 a x , b y a_x,b_y ax,by 在另一处(该调整所得到的另一个匹配位点)完成匹配,再将这个 ( a , b ) (a,b) (a,b) 移动到 ( ∅ ) (\varnothing) (∅) 位置。由于当前方案是最优,移动 ( a , b ) (a,b) (a,b) 到 ( ∅ ) (\varnothing) (∅) 必然是非正贡献,应当移除。
显然 ( a , b ) (a,b) (a,b) 被拆掉的时候, a a a 和 b b b 都需要重新进入一个匹配。因此拆掉两个 ( a , b ) (a,b) (a,b) 是无需考虑的。
于是维护 ( a ) (a) (a) 的 min a , max b \min a,\;\max b mina,maxb 和 ( b ) (b) (b) 的 min b , max a \min b,\;\max a minb,maxa 和 ( a , b ) (a,b) (a,b) 的 min ( a + b ) \min (a{+}b) min(a+b) 和 ( ∅ ) (\varnothing) (∅) 的 max ( a + b ) \max(a{+}b) max(a+b) 就行。时间复杂度 O ( n log n ) \mathcal O(n\log n) O(nlogn) 。
代码
#include <cstdio>
#include <algorithm> // Almighty XJX yyds!!!
#include <cstring> // oracle: ZXY yydBUS!!!
#include <cctype> // Huge Egg Dog eats me!!!
#include <queue>
#include <functional>
using llong = long long;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
# define rep0(i,a,b) for(int i=(a); i!=(b); ++i)
inline int readint(){
int a = 0, c = getchar(), f = 1;
for(; !isdigit(c); c=getchar()) if(c == '-') f = -f;
for(; isdigit(c); c=getchar()) a = a*10+(c^48);
return a*f;
}
const int MAXN = 200000;
using PII = std::pair<int,int>;
std::priority_queue<PII> pq[6];
bool usea[MAXN], useb[MAXN];
int a[MAXN], b[MAXN], both;
int ida[MAXN], idb[MAXN];
bool cmpa(const int &x, const int &y){ return a[x] > a[y]; }
bool cmpb(const int &x, const int &y){ return b[x] > b[y]; }
const llong INF = 1ll<<60;
# define pq5push(x) pq[5].push(PII{a[x]+b[x],x})
# define pq4push(x) pq[4].push(PII{-a[x]-b[x],x})
std::function<bool(int)> pred[6];
int main(){
pred[0] = pred[1] = [](const int &x){ return usea[x] && !useb[x]; };
pred[2] = pred[3] = [](const int &x){ return !usea[x] && useb[x]; };
pred[4] = [](const int &x){ return usea[x] && useb[x]; };
pred[5] = [](const int &x){ return !usea[x] && !useb[x]; };
for(int T=readint(); T; --T){
int n = readint(), k = readint(), L = readint();
memset(usea, false, n), memset(useb, false, n);
llong ans = 0; both = 0; // clear
rep0(i,0,n) a[ida[i] = i] = readint();
std::sort(ida, ida+n, cmpa);
rep0(i,0,k) usea[ida[i]] = true, ans += a[ida[i]];
rep0(i,0,n) b[idb[i] = i] = readint();
std::sort(idb, idb+n, cmpb);
rep0(i,0,k) useb[idb[i]] = true, ans += b[idb[i]];
rep(i,0,5) while(!pq[i].empty()) pq[i].pop();
rep0(i,0,n) // sample for classifing
if(usea[i] && useb[i]) pq4push(i), ++ both;
else if(!usea[i] && !useb[i]) pq5push(i);
else if(usea[i]){ // && !useb
pq[0].push(PII{-a[i],i});
pq[1].push(PII{b[i],i});
}
else{ // useb && !usea
pq[2].push(PII{a[i],i});
pq[3].push(PII{-b[i],i});
}
for(; both<L; ++both){
rep(i,0,5) while(!pq[i].empty()){
int x = pq[i].top().second;
if(!pred[i](x)) pq[i].pop(); else break;
}
llong ca = (pq[0].empty() || pq[2].empty()) ?
-INF : pq[0].top().first+pq[2].top().first;
llong cb = (pq[1].empty() || pq[3].empty()) ?
-INF : pq[1].top().first+pq[3].top().first;
llong mer = (pq[0].empty() || pq[3].empty() || pq[5].empty()) ?
-INF : pq[0].top().first+pq[3].top().first+pq[5].top().first;
llong sub = (pq[4].empty() || pq[1].empty() || pq[2].empty()) ?
-INF : pq[4].top().first+pq[1].top().first+pq[2].top().first;
if(ca >= cb && ca >= mer && ca >= sub){
int fr = pq[0].top().second, to = pq[2].top().second;
usea[fr] = false, usea[to] = true; ans += ca;
pq5push(fr), pq4push(to);
}
else if(cb >= ca && cb >= mer && cb >= sub){
int fr = pq[3].top().second, to = pq[1].top().second;
useb[fr] = false, useb[to] = true; ans += cb;
pq5push(fr), pq4push(to);
}
else if(mer >= ca && mer >= cb && mer >= sub){
int fra = pq[0].top().second, frb = pq[3].top().second;
int to = pq[5].top().second; ans += mer;
usea[fra] = useb[frb] = false, usea[to] = useb[to] = true;
pq5push(fra), pq5push(frb), pq4push(to);
}
else{ // substitute
int fr = pq[4].top().second; ans += sub;
int toa = pq[2].top().second, tob = pq[1].top().second;
usea[fr] = useb[fr] = false, usea[toa] = useb[tob] = true;
pq5push(fr), pq4push(toa), pq4push(tob);
}
}
printf("%lld\n",ans);
}
return 0;
}
后记
那么,能否建出网络流,以完全地佐证该做法的正确性呢?
其实 ( a ) , ( b ) → ( a , b ) (a),(b)\to(a,b) (a),(b)→(a,b) 这种 “给 a a a 换一个 b b b 来配对” 已经提示我们:将 a , b a,b a,b 建成两层(分层图)。
那么这实际上类似于二分图匹配。先用费用 0 0 0 的边满足题目的限制条件: a i → x a_i\to x ai→x 容量 + ∞ +\infty +∞, x → y x\to y x→y 容量 ( k − L ) (k{-}L) (k−L), y → b j y\to b_j y→bj 容量 + ∞ +\infty +∞ 。
补全整张图: a i → b i a_i\to b_i ai→bi 容量 1 1 1 费用 0 0 0,然后 S → a i S\to a_i S→ai 容量 1 1 1 费用 a i a_i ai,最后 b i → T b_i\to T bi→T 容量 1 1 1 费用 b i b_i bi 。
可以发现反悔贪心实际上是对 x → y x\to y x→y 退流。