「笔记」反悔贪心

到现在才学我是不是废了

很多思想都是看的题解,在此感谢各位写题解的神仙/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 - s) - (a_x + b_i - s) = b_j - b_i \]

\((a_x, b_j)\) 配对所产生的贡献是

\[b_j - s + a_x \]

因此每次我们把 \(-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;
}
posted @ 2022-01-06 15:17  Suzt_ilymtics  阅读(252)  评论(1编辑  收藏  举报