「笔记」反悔贪心
到现在才学我是不是废了
很多思想都是看的题解,在此感谢各位写题解的神仙/qq
这个题单不错 link
反悔贪心的核心思想是:当前的这次决策不一定最优,但我们先拿过来放着,如果以后找到更优的在把它换掉。这样的话只需要保证当前最优就可以了,如果以后更优我们会反悔。而这个过程通常用堆实现。
有几种比较常见的模型:
- 枚举次数限制元素
- 枚举所有元素限制次数
题型分类标准:
-
一种元素只有一单位单方向贡献但被时间限制(+ 选择个数限制)
-
一种元素有多单位贡献(+ 选择个数限制)
-
一种元素有多方向贡献(+ 选择个数限制)
大概做完下面的题后会有更细致的感受?
模板题目大体一般是这个样子:
你有 \(n\) 个物品需要修理,修理一个物品需要 \(1\) 单位时间,修理每个物品可获得 \(v_i\) 的价值,每个物品要在第 \(t_i\) 的时间前修理完成,否则报废。问最优价值。
套路的解法是
对所有物品按照时间排序。对于每个物品,如果有时间修就修,如果没时间了,就从已经修过的物品中挑一个价值最小的,如果当前的物品更优,就把这个价值最小的去掉换上这个新的。
对于价值最小的用一个小根堆来维护即可。
时间复杂度 \(\mathcal O(n \log n)\)。
下面这四个题都和上面一样套路。
P2949,P4053,P2107,P3545
CF865D Buy Low Sell High
这个题的转化挺妙的。
从前向后遍历,把每个值都压进小根堆里。
在这之前,如果这个值比小根堆里的最小值大,那么 ans += x - q.top()
,表明我要在那个最小值处买入,在这里卖出。然后在 q.push(x)
。
为什么还要在放进去一个 \(x\) 呢,如果后面遇到一个更优的 \(y\),我不在 \(x\) 处卖了,我在 \(y\) 处卖,那此时的贡献应该为 y-x
。
/*
Work by: Suzt_ilymtics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<bits/stdc++.h>
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
int n, ans = 0;
priority_queue<int, vector<int>, greater<int> > q;
int read() {
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0', ch = getchar();
return f ? -s : s;
}
signed main()
{
n = read();
for(int i = 1; i <= n; ++i) {
int x = read();
if(!q.empty() && q.top() < x) ans += x - q.top(), q.pop(), q.push(x);
q.push(x);
}
printf("%lld\n", ans);
return 0;
}
P1484,P1792,P3620,SP1553
这四个是四倍经验,题面略有不同,都是同一套路。
简述题意:
一个长度为 \(n\) 的序列 \(a\),选 \(k\) 个数,不能相邻,求和最大是多少。
不难发现,当 \(k=1\) 时,我们会取最大的 \(a_i\)。
当 \(k=2\) 时,有两种可能:
- 另取一个与 \(a_i\) 不相邻的 \(a_j\) 。
- 取 \(a_{i-1}\) 和 \(a_{i+1}\)。
我们可以发现,如果 \(k=1\) 时选了 \(a_i\),那么对于 \(a_{i-1}\) 和 \(a_{i+1}\) ,它们要么同时被选要么同时不选。
那考虑选了 \(a_{i-1}\) 和 \(a_{i+1}\) 的贡献,这个时候已经不能选 \(a_i\) 了,因此贡献为 \(a_{i-1} + a_{i+1} - a_i\)。
因此当 \(a_i\) 被选时,我们可以删去 \(a_{i-1}\) 和 \(a_{i+1}\) ,并把 \(a_i\) 改成 \(a_{i-1}+a_{i+1} - a_i\),重新找最大值。
维护当前序列的最大值可以用优先队列,维护当前序列的状态可以用双向链表。
时间复杂度为 \(\mathcal O(k \log n)\) 。
CF436E Cardboard Box
这个就比前面的复杂多了。
关键在于一次操作可以多单位的进行。
这里有两种做法
One:权衡
考虑不进行反悔贪心,直接权衡选一个两星关卡优还是两个一星关卡优。
开两个堆维护 \(a_i\) 和 \(b_i\) 的最小值。
设 \(q_a\) 堆的最小值为 \(a_{\min}\),\(q_b\) 堆的最小值为 \(b_{\min}\),可以假设他们不是同一个 \(i\),其实这个无所谓(
直接比较 \(a_{\min}\) 和 \(b_{\min}\) 是不对的,因为 \(a_{\min}\) 所对应的 \(b\) 值可能比 \(a_{\min}\) 大不了多少。
所以我们要取出 \(q_a\) 堆的次小值 \(a_{\min2}\) 。
- 当 \(a_{\min} + a_{\min2} \le b_{\min}\) 时:
选两个一星,然后把其对应的编号标记,如果在 \(q_b\) 堆中遇到就要跳过去。
把 \(b_{\min}\) 在放回去。
如果其中某个位置 \(i\) 是第一次选,再把 \(b_{i} - a_i\) 放进 \(q_a\) ,表示增加 \(b_i - a_i\) 的代价可以多得到 \(1\) 个星星,与普通星星无异。
- 当 \(a_{\min} + a_{\min2} > b_{\min}\) 时:
直接取一个 \(2\) 星,将 \(a_{\min}\) 和 \(a_{\min2}\) 重新入队。并在 \(q_a\) 中删除 \(b_{\min}\) 中所对应的点。
几个细节:
- 如果 \(w\) 是奇数则在最开始向 \(q_a\) 中塞一个编号为 \(0\) 权值为 \(0\) 的点,并把 \(0\) 号点标记为使用过一次,以免再次入堆。
- \(w=2 \times n\) 时进行特判全选 \(b\) 。
- 如果队中无元素 \(a_{\min}\) (或 \(b_{\min}\) )初值要付极大值。
- 取某个值之前先把已标记的在堆顶的点删除。
/*
Work by: Suzt_ilymtics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<bits/stdc++.h>
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 3e5+5;
const int INF = 2e9+7;
const int mod = 1e9+7;
struct node {
int val, id;
bool operator < (const node &b) const { return val > b.val; }
};
int n, K, ans = 0;
int a[MAXN], b[MAXN];
int cnt[MAXN];
priority_queue<node> qa, qb;
bool visa[MAXN], visb[MAXN];
int read() {
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0', ch = getchar();
return f ? -s : s;
}
signed main()
{
n = read(), K = read();
for(int i = 1; i <= n; ++i) a[i] = read(), b[i] = read(), qa.push((node){a[i], i}), qb.push((node){b[i], i});
if(K == 2 * n) {
for(int i = 1; i <= n; ++i) ans += b[i];
printf("%lld\n", ans);
for(int i = 1; i <= n; ++i) printf("2");
return puts(""), 0;
}
if(K & 1) ++ K, qa.push((node){0, 0}), cnt[0] = 1;
for(int i = 1; i <= K; i += 2) {
node a1, a2, b1;
while(!qa.empty() && visa[qa.top().id]) qa.pop();
!qa.empty() ? a1 = qa.top(), qa.pop(), 0 : a1.val = INF, 0;
while(!qa.empty() && visa[qa.top().id]) qa.pop();
!qa.empty() ? a2 = qa.top(), qa.pop(), 0 : a2.val = INF, 0;
while(!qb.empty() && visb[qb.top().id]) qb.pop();
!qb.empty() ? b1 = qb.top(), qb.pop(), 0 : b1.val = INF, 0;
if(a1.val + a2.val <= b1.val) {
visb[a1.id] = visb[a2.id] = true;
cnt[a1.id] ++, cnt[a2.id] ++;
if(cnt[a1.id] == 1) qa.push((node){b[a1.id] - a[a1.id], a1.id});
if(cnt[a2.id] == 1) qa.push((node){b[a2.id] - a[a2.id], a2.id});
qb.push(b1);
ans += a1.val + a2.val;
} else {
visa[b1.id] = true;
cnt[b1.id] += 2;
qa.push(a1), qa.push(a2);
ans += b1.val;
}
}
printf("%lld\n", ans);
for(int i = 1; i <= n; ++i) printf("%lld", cnt[i]);
puts("");
return 0;
}
Two:反悔
如果要用反悔贪心,最牛逼的地方就在于转化成一次操作只多得一个星。
一共分四种情况讨论。
如果直接贪有两种情况:
- 把一个零星变成一星,花费 \(a_i\)。
- 把一个一星变成两星,花费 \(b_i - a_i\)。
反悔也有两种情况:
- 把一个一星变成零星,然后选一个二星,花费 \(b_j - a_i\)。
- 把一个二星变成一星,然后选一个二星,花费 \(b_j + a_i - b_i\)。
因此为了维护到所有情况我们需要维护五个堆
分别是 \(a_i\),\(b_i-a_i\),\(-a_i\),\(b_i\),\(a_i-b_i\)。
分别代表了零星到一星,一星到二星,一星到零星,零星到二星,二星到一星的代价。
每次选择的时候,选四种情况的最小值即可。
上述两种做法复杂度均为 \(\mathcal O(n \log n)\) ,不同做法和实现所用常熟有较大略微差别
/*
Work by: Suzt_ilymtics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<bits/stdc++.h>
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 3e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
const int op1 = 0, op2 = 1, op3 = 1, op4 = 0, op5 = 2;
struct node {
int val, id;
bool operator < (const node &b) const { return val > b.val; }
};
int n, K, ans = 0;
int a[MAXN], b[MAXN], cnt[MAXN];
priority_queue<node> q1, q2, q3, q4, q5;
int read() {
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0', ch = getchar();
return f ? -s : s;
}
signed main()
{
n = read(), K = read();
for(int i = 1; i <= n; ++i) a[i] = read(), b[i] = read();
for(int i = 1; i <= n; ++i) q1.push((node){a[i], i}), q4.push((node){b[i], i});
for(int i = 1; i <= K; ++i) {
int Min = INF, opt = false, p, q;
while(!q1.empty() && cnt[q1.top().id] != op1) q1.pop();
if(!q1.empty() && q1.top().val < Min) {
Min = q1.top().val;
p = q1.top().id;
opt = 1;
}
while(!q2.empty() && cnt[q2.top().id] != op2) q2.pop();
if(!q2.empty() && q2.top().val < Min) {
Min = q2.top().val;
p = q2.top().id;
opt = 2;
}
while(!q3.empty() && cnt[q3.top().id] != op3) q3.pop();
while(!q4.empty() && cnt[q4.top().id] != op4) q4.pop();
if(!q3.empty() && !q4.empty() && q3.top().val + q4.top().val < Min) {
Min = q3.top().val + q4.top().val;
p = q3.top().id, q = q4.top().id;
opt = 3;
}
while(!q4.empty() && cnt[q4.top().id] != op4) q4.pop();
while(!q5.empty() && cnt[q5.top().id] != op5) q5.pop();
if(!q4.empty() && !q5.empty() && q4.top().val + q5.top().val < Min) {
Min = q4.top().val + q5.top().val;
q = q4.top().id, p = q5.top().id;
opt = 4;
}
ans += Min;
if(opt == 1) {
cnt[p] ++;
q2.push((node){b[p] - a[p], p});
q3.push((node){-a[p], p});
} else if(opt == 2) {
cnt[p] ++;
q5.push((node){a[p] - b[p], p});
} else if(opt == 3) {
cnt[p] -- , cnt[q] += 2;
q1.push((node){a[p], p});
q5.push((node){a[q] - b[q], q});
} else if(opt == 4) {
cnt[p] --, cnt[q] += 2;
q2.push((node){b[p] - a[p], p});
q3.push((node){- a[p], p});
q5.push((node){a[q] - b[q], q});
}
}
printf("%lld\n", ans);
for(int i = 1; i <= n; ++i) printf("%lld", cnt[i]);
return 0;
}
CF730I Olympiad in Programming and Sports
- 一种元素有多方向贡献(+ 选择个数限制)
做法好像还蛮多的说?
费用流?DP?贪心也有很多方向。
这里只介绍一种(只学了一种
考虑先把编程团队都填满,选 \(p\) 个最大的 \(a_i\)。
然后再选 \(s\) 个人加入到体育团队中。
显然有两种选择:
- 直接从未选的人中选一个加进去,贡献为 \(b_i\)
- 从已加入编程团队的人中选一个加入到体育团队中,在从未选的人中选一个加入编程团队中,贡献为 \(a_j + b_i - a_i\)
因此我们需要维护三个堆,分别维护 \(a_i\),,\(b_i\),\(b_i - a_i\)。
分别代表一个人加入编程的贡献,加入体育的贡献,从编程转到体育的贡献。
复杂度为 \(\mathcal O(n \log n)\)。
/*
Work by: Suzt_ilymtics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
struct node {
int val, id;
bool operator < (const node &b) const { return val > b.val; }
bool operator > (const node &b) const { return val < b.val; }
}a[MAXN], b[MAXN];
bool cmp(node x, node y) { return x.id < y.id; }
int n, P, S, ans = 0;
priority_queue<node, vector<node>, greater<node> > q1, q2, q3;
int vis[MAXN];
int read() {
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0', ch = getchar();
return f ? -s : s;
}
int main()
{
n = read(), P = read(), S = read();
for(int i = 1; i <= n; ++i) a[i].val = read(), a[i].id = i;
for(int i = 1; i <= n; ++i) b[i].val = read(), b[i].id = i;
sort(a + 1, a + n + 1);
for(int i = 1; i <= P; ++i) vis[a[i].id] = true, ans += a[i].val;
sort(a + 1, a + n + 1, cmp);
for(int i = 1; i <= n; ++i) {
q1.push((node){a[i].val, i});
q2.push((node){b[i].val, i});
q3.push((node){b[i].val - a[i].val, i});
}
for(int i = 1; i <= S; ++i) {
int Max = 0, opt = 0, p, q;
while(!q2.empty() && vis[q2.top().id] != 0) q2.pop();
if(!q2.empty() && q2.top().val > Max) {
Max = q2.top().val;
opt = 1, p = q2.top().id;
}
while(!q1.empty() && vis[q1.top().id] != 0) q1.pop();
while(!q3.empty() && vis[q3.top().id] != 1) q3.pop();
if(!q1.empty() && !q3.empty() && q1.top().val + q3.top().val > Max) {
Max = q1.top().val + q3.top().val;
opt = 2, p = q1.top().id, q = q3.top().id;
}
ans += Max;
if(opt == 1) {
q2.pop(), vis[p] = 2;
} else {
q1.pop(), q3.pop();
vis[p] = 1, vis[q] = 2;
q3.push((node){b[p].val - a[p].val, p});
}
}
printf("%d\n", ans);
for(int i = 1; i <= n; ++i) if(vis[i] == 1) printf("%d ", i); puts("");
for(int i = 1; i <= n; ++i) if(vis[i] == 2) printf("%d ", i); puts("");
return 0;
}
AT2672 [AGC018C] Coins
这个题的每个元素有三个方向可以选择,所幸的是还有另外一个限制条件 \(n = X + Y + Z\)。
否则可能会更麻烦?
一开始我考虑的是先选最大的 \(X\) 个金币,在按照上面的套路选贪 \(Y\) 个银币,在按照上面的套路贪 \(Z\) 个铜币。
但这样考虑会漏掉很多情况,例如在贪铜币的过程中,对于某个人,可以选其金币,让金币中的某个人选银币,让银币中的某个人选铜币。
为了用上 \(n = X + Y + Z\) 的条件,就有了这个做法。
把 \((a,b,c)\) 三元组替换为 \((e=b-a, f=c-a)\) 二元组。
即默认所有人拿金币,然后在 \(n\) 个人中选 \(Y\) 个拿银币,选 \(Z\) 个拿铜币。
剩下的做法就和上一个题一样啦。
另一种做法是:先随便分配。
然后考虑每次变换去调整。
一共有五种情况:
\(x \to y \to z \to x\)
\(x \to z \to y \to x\)
\(x \to y \to x\)
\(x \to z \to x\)
\(y \to z \to y\)
开六个堆维护即可。
感觉蛮麻烦的,我也就没有写
时间复杂度都是 \(\mathcal O(n \log n)\) ,当然有常数上的差距。
/*
Work by: Suzt_ilymtics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e12+7;
const int mod = 1e9+7;
struct node {
int val, id;
bool operator < (const node &b) const { return val < b.val; }
}a[MAXN], b[MAXN], c[MAXN];
int n, X, Y, Z, ans = 0;
int vis[MAXN];
priority_queue<node> q1, q2, q3;
int read() {
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0', ch = getchar();
return f ? -s : s;
}
bool cmp1(node x, node y) { return x.val > y.val; }
bool cmp2(node x, node y) { return x.id < y.id; }
signed main()
{
X = read(), Y = read(), Z = read();
n = X + Y + Z;
for(int i = 1; i <= n; ++i) {
a[i].val = read(), b[i].val = read(), c[i].val = read();
a[i].id = b[i].id = c[i].id = i;
ans += a[i].val;
}
for(int i = 1; i <= n; ++i) {
int x = b[i].val;
b[i].val = c[i].val - a[i].val;
a[i].val = x - a[i].val;
}
sort(a + 1, a + n + 1, cmp1);
for(int i = 1; i <= Y; ++i) vis[a[i].id] = 1, ans += a[i].val;
sort(a + 1, a + n + 1, cmp2);
for(int i = 1; i <= n; ++i) {
q1.push((node){a[i].val, i});
q2.push((node){b[i].val, i});
q3.push((node){b[i].val - a[i].val, i});
}
for(int i = 1; i <= Z; ++i) {
int Max = -INF, opt, p, q;
while(!q2.empty() && vis[q2.top().id] != 0) q2.pop();
if(!q2.empty() && q2.top().val > Max) Max = q2.top().val, p = q2.top().id, opt = 1;
while(!q1.empty() && vis[q1.top().id] != 0) q1.pop();
while(!q3.empty() && vis[q3.top().id] != 1) q3.pop();
if(!q1.empty() && !q3.empty() && q1.top().val + q3.top().val > Max) {
Max = q1.top().val + q3.top().val;
p = q1.top().id, q = q3.top().id, opt = 2;
}
ans += Max;
if(opt == 1) {
vis[p] = 2, q2.pop();
} else {
vis[p] = 1, vis[q] = 2, q1.pop(), q3.pop();
q3.push((node){b[p].val - a[p].val, p});
}
}
printf("%lld\n", ans);
return 0;
}
CF802N April Fools' Problem (medium)
题解区的做法只有费用流。不过费用流的本质就是反悔的思想。
设 \(n\) 个中转点,\(i\) 连向 \(i + 1\) 流量为 \(INF\) 花费为 \(0\) 。
对于每个 \(a_i\),源点向 \(i\) 连边,流量为 \(1\) 花费为 \(a_i\)。
对于每个 \(b_i\),\(i\) 想 \(T\) 连边,流量为 \(1\) 花费为 \(b_i\)。
怎么控制只选 \(k\) 个点?在另设一个汇点 \(G\) ,然后 \(T\) 向 \(G\) 连一条流量为 \(k\) 花费为 \(0\) 的点。
时间复杂度为 \(\mathcal O(nmf)\),但费用流一般跑不满,因此能卡过去。
CF802O April Fools' Problem (hard)
这道题是前面那道题的加强版。
显然这个数据范围费用流就跑不过去了。
由于最小费用最大流的每次增广值单调不减,从而随着 \(k\) 的增大,\(f(k)-f(k-1)\) 单调不减,即 \(y=f(x)\) 下凸。所以可以考虑 \(\text{wqs}\) 二分。
我们二分一个斜率 \(s\) ,表示切线的斜率。
然后我们采用和CF865D这个题类似的套路。
将匹配 \((a_x, b_i)\) 换成 \((a_x, b_j)\) 时所产生的贡献是:
将 \((a_x, b_j)\) 配对所产生的贡献是
因此每次我们把 \(-a_i\) 扔进堆里,维护一个大根堆。
每次取出堆顶的值 \(v\),如果 \(b_i - s - v < 0\) 就相当于完成一次匹配或者一次替换,加上它的贡献然后将 \(b_i - s\) 入堆。堆内的每个值都要标记一下它是已匹配的还是未匹配的。
如果堆内已匹配的数量 \(=k\) ,直接输出记录的答案 \(ans + k \times s\) 即可。
否则调整边界 \(l, r\)。
时间复杂度 \(\mathcal O(n \log n \log V)\)。
另外这篇题解link用了最短路优化费用流,复杂度是吊打上面算法的 \(\mathcal O(k \log n)\)。
/*
Work by: Suzt_ilymtics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 5e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
struct node {
int val, type;
bool operator < (const node &b) const { return val < b.val; }
};
int n, K, sum, cnt;
int a[MAXN], b[MAXN];
priority_queue<node> q;
int read() {
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0', ch = getchar();
return f ? -s : s;
}
signed main()
{
n = read(), K = read();
for(int i = 1; i <= n; ++i) a[i] = read();
for(int i = 1; i <= n; ++i) b[i] = read();
int l = 0, r = 2e9, mid;
while(l <= r) {
// cout << l << " " << mid << " " << r << "\n";
mid = l + r >> 1, sum = cnt = 0;
for(int i = 1; i <= n; ++i) {
q.push((node){- a[i], 0});
int tmp = b[i] - mid - q.top().val;
if(tmp < 0) sum += tmp, q.pop(), q.push((node){b[i] - mid, 1});
}
while(!q.empty()) cnt += q.top().type, q.pop();
if(cnt == K) return printf("%lld\n", sum + K * mid), 0;
if(cnt < K) l = mid + 1;
else r = mid - 1;
}
return 0;
}
P5470 [NOI2019] 序列
djy 神仙的题解讲的太明白了%%%
如果 \(l\) 个公共部分已经确定了,那么直接选 \(k-l\) 个最大的就是最优的。
那么我们先选 \(k\) 个最大的,然后通过反悔贪心,每一步让公共部分多一个,直到变成 \(l\) 个,这样就是最优的。
考虑有哪几种操作能让公共部分多一个。
- 找一个选了的 \(a_i\),和一个选了的 \(b_j\),然后用 \(b_i\) 代替 \(b_j\),代价是 \(b_i - b_j\)。
- 找一个选了的 \(b_i\),和一个选了的 \(a_j\),然后用 \(a_i\) 代替 \(a_j\),代价是 \(a_i - a_j\)。
- 找一个选了的 \(a_i\) 和 \(b_j\),再找一个没选的位置 \(p\),选 \(a_p\) 和 \(b_p\) 代替 \(a_i\) 和 \(b_j\),代价是 \(a_p + b_p - a_i - b_j\)。
- 找一个选了的 \(a_i\) 和 \(b_j\),再找一个都选的位置 \(p\),选 \(b_i\) 和 \(a_j\) 代替 \(a_p\) 和 \(b_p\),代价是 \(b_i + a_j - a_p - b_p\)。
拆一下上面的式子,用六个堆分别维护 \(a_i\),\(-a_i\),\(b_i\),\(-b_i\),\(a_i+b_i\),\(-a_i-b_i\)。
时间复杂度 \(\mathcal O(n \log n)\)。
/*
Work by: Suzt_ilymtics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 2e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
struct node {
int val, id;
bool operator < (const node &b) const { return val < b.val; }
}a[MAXN], b[MAXN];
int T, n, K, L, ans = 0;
int vis[MAXN];
priority_queue<node> q1, q2, q3, q4, q5, q6;
int read() {
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0', ch = getchar();
return f ? -s : s;
}
bool cmp1(node x, node y) { return x.val > y.val; }
bool cmp2(node x, node y) { return x.id < y.id; }
void Clear() {
while(!q1.empty()) q1.pop();
while(!q2.empty()) q2.pop();
while(!q3.empty()) q3.pop();
while(!q4.empty()) q4.pop();
while(!q5.empty()) q5.pop();
while(!q6.empty()) q6.pop();
memset(vis, false, sizeof vis);
ans = 0;
}
signed main()
{
T = read();
while(T--) {
Clear();
n = read(), K = read(), L = read();
for(int i = 1; i <= n; ++i) a[i].val = read(), a[i].id = i;
for(int i = 1; i <= n; ++i) b[i].val = read(), b[i].id = i;
sort(a + 1, a + n + 1, cmp1), sort(b + 1, b + n + 1, cmp1);
for(int i = 1; i <= K; ++i) {
ans += a[i].val + b[i].val;
vis[a[i].id] ++, vis[b[i].id] += 2;
}
for(int i = 1; i <= n; ++i) L -= (vis[i] == 3);
sort(a + 1, a + n + 1, cmp2), sort(b + 1, b + n + 1, cmp2);
for(int i = 1; i <= n; ++i) {
q1.push((node){a[i].val, i});
q2.push((node){-a[i].val, i});
q3.push((node){b[i].val, i});
q4.push((node){-b[i].val, i});
q5.push((node){a[i].val + b[i].val, i});
q6.push((node){- a[i].val - b[i].val, i});
}
while(L-- > 0) {
int Max = -INF, i, j, k, opt = 0;
while(!q3.empty() && vis[q3.top().id] != 1) q3.pop();
while(!q4.empty() && vis[q4.top().id] != 2) q4.pop();
if(!q3.empty() && !q4.empty()) {
int res = q3.top().val + q4.top().val;
if(res > Max) {
Max = res;
i = q3.top().id, j = q4.top().id;
opt = 1;
}
}
while(!q1.empty() && vis[q1.top().id] != 2) q1.pop();
while(!q2.empty() && vis[q2.top().id] != 1) q2.pop();
if(!q1.empty() && !q2.empty()) {
int res = q1.top().val + q2.top().val;
if(res > Max) {
Max = res;
i = q1.top().id, j = q2.top().id;
opt = 2;
}
}
while(!q5.empty() && vis[q5.top().id] != 0) q5.pop();
while(!q2.empty() && vis[q2.top().id] != 1) q2.pop();
while(!q4.empty() && vis[q4.top().id] != 2) q4.pop();
if(!q5.empty() && !q2.empty() && !q4.empty()) {
int res = q5.top().val + q2.top().val + q4.top().val;
if(res > Max) {
Max = res;
i = q5.top().id, j = q2.top().id, k = q4.top().id;
opt = 3;
}
}
while(!q6.empty() && vis[q6.top().id] != 3) q6.pop();
while(!q1.empty() && vis[q1.top().id] != 2) q1.pop();
while(!q3.empty() && vis[q3.top().id] != 1) q3.pop();
if(!q6.empty() && !q1.empty() && !q3.empty()) {
int res = q6.top().val + q1.top().val + q3.top().val;
if(res > Max) {
Max = res;
i = q6.top().id, j = q1.top().id, k = q3.top().id;
opt = 4;
}
}
ans += Max;
if(opt == 1) {
vis[i] += 2, vis[j] -= 2;
q3.pop(), q4.pop();
q5.push((node){a[j].val + b[j].val, j});
q6.push((node){- a[i].val - b[i].val, i});
} else if(opt == 2) {
vis[i] += 1, vis[j] -= 1;
q1.pop(), q2.pop();
q5.push((node){a[j].val + b[j].val, j});
q6.push((node){- a[i].val - b[i].val, i});
} else if(opt == 3) {
vis[i] += 3, vis[j] -= 1, vis[k] -= 2;
q5.pop(), q2.pop(), q4.pop();
q6.push((node){- a[i].val - b[i].val, i});
q5.push((node){a[j].val + b[j].val, j});
q5.push((node){a[k].val + b[k].val, k});
} else {
vis[i] -= 3, vis[j] += 1, vis[k] += 2;
q5.push((node){a[i].val + b[i].val, i});
q6.push((node){- a[j].val - b[j].val, j});
q6.push((node){- a[k].val - b[k].val, k});
}
}
printf("%lld\n", ans);
}
return 0;
}
CF280D k-Maximum Subsequence Sum
这个题的代码真 tm 恶心/tuu
这个题的思路和种树挺像的。
考虑 \(k=1\) 的情况,肯定选 \([l,r]\) 中的最大子段和 \([l_1, r_1]\)。
考虑 \(k=2\) 的情况,一种情况是选 \([l,l_1)\) 或者 \((r_1, r]\) 中的最大子段和,另一种情况是减去选 \([l_1,r_1]\) 中的最小子段和,即把原来的 \([l_1, r_1]\) 分成两段。
因此如果再选了 \([l_1, r_1]\) 后将这一段区间取反,然后再查询最大子段和,求的最大子段和如果包括这些被取反的数,就相当于撤销了这些方案。那么这样的得到的一定是最优解。
对于每个询问直接操作 \(k\) 次即可。如果某次的最大字段和 \(\le 0\),可以直接退出。
时间复杂度 \(\mathcal O(mk \log n)\)。
/*
Work by: Suzt_ilymtics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
int n, m, ans, sc;
int a[MAXN];
int read() {
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0', ch = getchar();
return f ? -s : s;
}
namespace Seg {
#define lson i << 1
#define rson i << 1 | 1
struct Tree {
int l, r, sum, maxv, maxl, maxr, minv, minl, minr, lmax, lmaxid, lmin, lminid, rmax, rmaxid, rmin, rminid;
bool rev;
// Tree () { l = r = sum = maxv = maxl = maxr = minv = minl = minr = lmax = lmaxid = lmin = lminid = rmax = rmaxid = rmin = rminid = rev = 0; }
Tree operator + (const Tree &b) const {
Tree res;
res.l = this->l, res.r = b.r;
res.sum = this->sum + b.sum;
if(this->lmax > this->sum + b.lmax) res.lmax = this->lmax, res.lmaxid = this->lmaxid;
else res.lmax = this->sum + b.lmax, res.lmaxid = b.lmaxid;
if(this->lmin < this->sum + b.lmin) res.lmin = this->lmin, res.lminid = this->lminid;
else res.lmin = this->sum + b.lmin, res.lminid = b.lminid;
if(this->rmax + b.sum < b.rmax) res.rmax = b.rmax, res.rmaxid = b.rmaxid;
else res.rmax = this->rmax + b.sum, res.rmaxid = this->rmaxid;
if(this->rmin + b.sum > b.rmin) res.rmin = b.rmin, res.rminid = b.rminid;
else res.rmin = this->rmin + b.sum, res.rminid = this->rminid;
res.maxv = this->rmax + b.lmax, res.maxl = this->rmaxid, res.maxr = b.lmaxid;
if(res.maxv < this->maxv) res.maxv = this->maxv, res.maxl = this->maxl, res.maxr = this->maxr;
if(res.maxv < b.maxv) res.maxv = b.maxv, res.maxl = b.maxl, res.maxr = b.maxr;
res.minv = this->rmin + b.lmin, res.minl = this->rminid, res.minr = b.lminid;
if(res.minv > this->minv) res.minv = this->minv, res.minl = this->minl, res.minr = this->minr;
if(res.minv > b.minv) res.minv = b.minv, res.minl = b.minl, res.minr = b.minr;
res.rev = false;
return res;
}
}tree[MAXN << 2], tmp[50];
void Push_up(int i) { tree[i] = tree[lson] + tree[rson]; }
void Rev(int i) {
tree[i].rev ^= 1;
tree[i].sum = - tree[i].sum;
swap(tree[i].lmax, tree[i].lmin), swap(tree[i].lmaxid, tree[i].lminid);
tree[i].lmax = - tree[i].lmax, tree[i].lmin = - tree[i].lmin;
swap(tree[i].rmax, tree[i].rmin), swap(tree[i].rmaxid, tree[i].rminid);
tree[i].rmax = - tree[i].rmax, tree[i].rmin = - tree[i].rmin;
swap(tree[i].maxv, tree[i].minv), swap(tree[i].maxl, tree[i].minl), swap(tree[i].maxr, tree[i].minr);
tree[i].maxv = - tree[i].maxv, tree[i].minv = - tree[i].minv;
}
void Build(int i, int l, int r) {
tree[i].l = l, tree[i].r = r;
if(l == r) {
tree[i].sum = tree[i].maxv = tree[i].minv = tree[i].lmax = tree[i].lmin = tree[i].rmax = tree[i].rmin = a[l];
tree[i].l = tree[i].r = tree[i].maxl = tree[i].maxr = tree[i].minl = tree[i].minr = tree[i].lmaxid = tree[i].lminid = tree[i].rmaxid = tree[i].rminid = l;
tree[i].rev = false;
return ;
}
int mid = (l + r) >> 1;
Build(lson, l, mid), Build(rson, mid + 1, r);
Push_up(i);
}
void Push_down(int i) {
if(!tree[i].rev) return ;
Rev(lson), Rev(rson);
tree[i].rev = false;
}
void Reverse(int i, int l, int r, int L, int R) {
if(L <= l && r <= R) return (void)(Rev(i));
Push_down(i);
int mid = (l + r) >> 1;
if(mid >= L) Reverse(lson, l, mid, L, R);
if(mid < R) Reverse(rson, mid + 1, r, L, R);
Push_up(i);
}
void Modify(int i, int l, int r, int pos, int val) {
if(l == r) return (void)(tree[i].sum = tree[i].maxv = tree[i].minv = tree[i].lmax = tree[i].lmin = tree[i].rmax = tree[i].rmin = val);
Push_down(i);
int mid = (l + r) >> 1;
if(mid >= pos) Modify(lson, l, mid, pos, val);
else Modify(rson, mid + 1, r, pos, val);
Push_up(i);
}
Tree Query(int i, int l, int r, int L, int R) {
if(L <= l && r <= R) return tree[i];
Push_down(i);
int mid = (l + r) >> 1, p = false; Tree ans;
if(mid >= L) ans = Query(lson, l, mid, L, R), p = true;
if(mid < R) {
Tree res = Query(rson, mid + 1, r, L, R);
ans = p ? ans + res : res;
}
return ans;
}
}
using namespace Seg;
int main()
{
n = read();
for(int i = 1; i <= n; ++i) a[i] = read();
Build(1, 1, n);
m = read();
for(int i = 1, opt, l, r, x, v, k; i <= m; ++i) {
opt = read();
if(!opt) {
x = read(), v = read();
Modify(1, 1, n, x, v);
} else {
l = read(), r = read(), k = read();
ans = 0, sc = 0;
for(int j = 1; j <= k; ++j) {
tmp[++sc] = Query(1, 1, n, l, r);
if(tmp[sc].maxv <= 0) {sc --; break;}
ans += tmp[sc].maxv;
Reverse(1, 1, n, tmp[sc].maxl, tmp[sc].maxr);
}
for(int j = 1; j <= sc; ++j) Reverse(1, 1, n, tmp[j].maxl, tmp[j].maxr);
printf("%d\n", ans);
}
}
return 0;
}