贪心法_2 2016.5.18


51Nod贪心入门教程_任务执行顺序

有N个任务需要执行,第i个任务计算时占R[i]个空间,而后会释放一部分
最后储存计算结果需要占据O[i]个空间(O[i] < R[i])

例如:
执行需要5个空间,最后储存需要2个空间
给出N个任务执行和存储所需的空间,问执行所有任务最少需要多少空间


分析:
本题可以抽象成,从一个整数开始,每次减去a,再加上b (a,b都是正数),要求每次操作都不产生负数

针对本题a[i] = R[i], b[i] = R[i] – O[i],注意O[i] < R[i],我们有0<b[i]<a[i]
所以尽管每次有减有加,但是加的没有减的多,总数还是在不断减小的
关键我们是要“最有利”的一种执行顺序
大家可以尝试多种贪心策略

我们给出标准答案——按照b[i]不增的顺序排序,是最“有利”的

为了定义“有利”,我们这样证明我们的结论:

如果对于b[0]>=b[1] >=…>=b[x] < b[x + 1]
(a[0],b[0])….(a[x], b[x]) (a[x + 1], b[x + 1])的组合可以不产生负数,则我们交换b[x]和b[x + 1]也可以不产生负数

证明:
交换(a[x], b[x])和(a[x + 1], b[x + 1])对x + 1更有利了,因为每个括号实际上是一个负数,所以越早安排这个括号,被减数就越大,就越不容易形成负数
关键看(a[x],b[x])移动到后面会不会产生负数

那其实是看之前的结果 -a[x + 1] + b[x + 1] – a[x]会不会产生负数
(注意-a[x + 1] + b[x + 1]不会产生负数,因为我们刚才已经证明了,对x + 1更有利)

而我们知道之前的结果-a[x] + b[x] – a[x + 1]不会产生负数(因为我们的假设就是这样)
而b[x + 1] > b[x],所以前者更大,所以-a[x + 1] + b[x + 1] – a[x]不会产生负数

因此我们证明了交换之后仍然不产生负数,也就是原先不产生负数,我们交换后仍然不产生负数

而经过若干次这样的交换之后,我们肯定会把序列交换成按照b的不增顺序排序的
从而我们证明了,任何可行的方案都不好于按照b不增顺序排序的序列执行的方案,从而证明了我们的贪心策略是有效的

很奇怪的策略——我们只考虑了b,居然能得到最优策略
可见贪心算法还是需要感觉,大胆假设,小心求证


输入
第1行:1个数N,表示任务的数量。(2 <= N <= 100000)
第2 - N + 1行:每行2个数R[i]和O[i],分别为执行所需的空间和存储所需的空间。(1 <= O[i] < R[i] <= 10000)

输出
输出执行所有任务所需要的最少空间

输入示例
20
14 1
2 1
11 3
20 4
7 5
6 5
20 7
19 8
9 4
20 10
18 11
12 6
13 12
14 9
15 2
16 15
17 15
19 13
20 2
20 1

输出示例
135


#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <queue>
#include <vector>
#include <stack>

using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef unsigned int uint;

const int INF = 0x3f3f3f3f;
const int maxn = 1e5 + 10;

struct Node {
    int R, O, b;
};

Node node[maxn];

int cmp_num(const void* a, const void* b);

int main()
{
#ifdef __AiR_H
    freopen("in.txt", "r", stdin);
#endif // __AiR_H
    int N;
    scanf("%d", &N);
    for (int i = 0; i < N; ++i) {
        scanf("%d%d", &node[i].R, &node[i].O);
        node[i].b = node[i].R - node[i].O;
    }
    int Max = 0;
    qsort(node, N, sizeof(node[0]), cmp_num);
    int Now_V = 0;
    for (int i = 0; i < N; ++i) {
        Now_V += node[i].R;
        if (Now_V > Max) {
            Max = Now_V;
        }
        Now_V -= node[i].b;
    }
    printf("%d\n", Max);
    return 0;
}

int cmp_num(const void* a, const void* b)
{
    Node* x = (Node*)a;
    Node* y = (Node*)b;
    return (y->b - x->b) > 0 ? 1 : -1;
}


51Nod贪心的经典算法_贪心算法概述


贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择

也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性

即某个状态以前的过程不会影响以后的状态,只与当前状态有关


贪心算法具有最优子问题结构,它的特点是“短视”,每次选择对当前局面最有利的决策,来一步步获得最优解

我个人认为,贪心不是一个具体的方法,而是一类方法,贪心算法的关键不在于想到,而在于正确性的证明

要证明一个贪心算法是正确的,需要证明我们可以把一个最优解逐步转化为我们用贪心算法所得到的解,而解不会更差

从而证明贪心算法得到的解和最优解是一样好的(显然,最优解不可能更好)

而要证明一个贪心算法是错误的,只需要找到一个反例就可以了

通常情况下,证明贪心算法是正确的或者找到贪心算法的一个反例都不那么容易

而且即使对于同一个问题,从不同角度的贪心算法的正确性也不尽相同

例如Dijkstra算法是著名的求单源图最短路径的贪心算法

如果我们也给出一个贪心算法,从源头开始每次选择最短的边继续走,直到走到,直到经过全部点或者无路可走

按照我们的算法,在上图中,A到C的最短路是A-B-C,它的长度是5,显然A-D-C才是真正的最短路

我们的贪心算法是错的

所以一般对于一个问题来说,我们只讲这样一个贪心算法是错误的,而不说这个问题不能采用贪心算法——因为可能从别的角度设计出的贪心算法是正确


一个问题即使不能使用某个贪心算法,也可以通过贪心算法给出一个“还说得过去”的解,这也是贪心算法在现实中存在的意义之一

基本的算法中贪心著名的贪心算法包括:

Dijskstr单源图最短路径算法、Prim和Kruskal最小生成树算法、Huffman编码简单压缩算法等

如果给贪心算法一个抽象地描述,我认为可以这样讲:

假设一些对象的集合 S, 每个对象 x 对应一个收益 payoff(x),对于任意 S 的子集 T,我们有一个函数可以判断它是否合法 isValid(T) ——它返回布尔值

并且这个函数通常有个性质,空集是合法的;

如果 T合法,它的任意子集都合法;

如果它非法,它的任意超集都非法

我们的目标是从 S 中选取若干个对象,形成一个集合 V,使得 isValid(V) == true 并且 payoff(V) 尽可能大

其中 payoff(V) 定义为V中所有对象的收益之和

贪心算法是这么解决这个问题的,从空集合开始,每次选一个 payoff 最大并且合法的对象 x 加入到 V 里面, V = VU{x}


可见具备上述性质的问题实际上还是比较特殊的,而上述性质通常成为贪心选择性

可见贪心选择是比较“短视”的,选取最优的一个元素,即使有多个,任选一个

而动态规划算法是从所有能达到当前状态的状态和决策中选取

所以从某种角度上讲,动态规划是枚举——只是聪明点的枚举罢了,它枚举的是所有状态以及该状态下的决策

而贪心只是单一的选择,盲目选择当前最优的决策

贪心和动态规划算法的比较可见下表:


51Nod贪心的经典算法_Prim算法

最小生成树的Prim算法也是贪心算法的一大经典应用

Prim算法的特点是时刻维护一棵树,算法不断加边,加的过程始终是一棵树

Prim算法过程:

一条边一条边地加, 维护一棵树

初始 E = {}空集合, V = {任意节点}

循环(n – 1)次,每次选择一条边(v1,v2), 满足:v1属于V , v2不属于V。且(v1,v2)权值最小

E = E + (v1,v2)
V = V + v2

最终E中的边是一棵最小生成树, V包含了全部节点


以下图为例介绍Prim算法的执行过程


Prim算法的过程从A开始 V = {A}, E = {}


选中边AF , V = {A, F}, E = {(A,F)}

选中边FB, V = {A, F, B}, E = {(A,F), (F,B)}


选中边BD, V = {A, B, F, D},   E = {(A,F), (F,B), (B,D)}

选中边DE, V = {A, B, F, D, E},   E = {(A,F), (F,B), (B,D), (D,E)}

选中边BC, V = {A, B, F, D, E, c},   E = {(A,F), (F,B), (B,D), (D,E), (B,C)}, 算法结束


Prim算法的证明:

假设Prim算法得到一棵树P,有一棵最小生成树T

假设P和T不同,我们假设Prim算法进行到第(K – 1)步时选择的边都在T中,这时Prim算法的树是P’

第K步时,Prim算法选择了一条边e = (u, v)不在T中

假设u在P’中,而v不在

因为T是树,所以T中必然有一条u到v的路径

我们考虑这条路径上第一个点u在P’中,最后一个点v不在P’中,则路径上一定有一条边f = (x,y),x在P’中,而且y不在P’中
我们考虑f和e的边权w(f)与w(e)的关系:

若w(f) > w(e),在T中用e换掉f (T中加上e去掉f),得到一个权值和更小的生成树,与T是最小生成树矛盾
若w(f) < w(e), Prim算法在第K步时应该考虑加边f,而不是e,矛盾

因此只有w(f) = w(e),我们在T中用e换掉f,这样Prim算法在前K步选择的边在T中了,有限步之后把T变成P,而树权值和不变, 从而Prim算法是正确的

请仔细理解Prim算法——时刻维护一棵生成树

我们的证明构造性地证明了所有地最小生成树地边权(多重)集合都相同!


输入
第1行:2个数N,M中间用空格分隔,N为点的数量,M为边的数量。(2 <= N <= 1000, 1 <= M <= 50000)
第2 - M + 1行:每行3个数S E W,分别表示M条边的2个顶点及权值。(1 <= S, E <= N,1 <= W <= 10000)

输出
输出最小生成树的所有边的权值之和。

输入示例
9 14
1 2 4
2 3 8
3 4 7
4 5 9
5 6 10
6 7 2
7 8 1
8 9 7
2 8 11
3 9 2
7 9 6
3 6 4
4 6 14
1 8 8

输出示例
37

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <queue>
#include <vector>
#include <stack>

using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef unsigned int uint;

const int INF = 0x3f3f3f3f;
const int maxn = 1e3 + 10;
int G[maxn][maxn];
int lowcost[maxn];
bool vis[maxn];
int N;

int Prim(void);

int main()
{
#ifdef __AiR_H
    freopen("in.txt", "r", stdin);
#endif // __AiR_H
    int M;
    scanf("%d%d", &N, &M);
    for (int i = 1; i <= N; ++i) {
        for (int j = 1; j <= N; ++j) {
            if (i == j) {
                G[i][j] = 0;
            } else {
                G[i][j] = INF;
            }
        }
    }
    int S, E, W;
    for (int i = 0; i < M; ++i) {
        scanf("%d%d%d", &S, &E, &W);
        G[S][E] = G[E][S] = W;
    }
    printf("%d\n", Prim());
    return 0;
}

int Prim(void)
{
    int sum = 0;
    memset(vis, false, sizeof(vis));
    for (int i = 2; i <= N; ++i) {
        lowcost[i] = G[1][i];
    }
    for (int i = 0; i < N-1; ++i) {
        int Min = INF;
        int t;
        for (int j = 2; j <= N; ++j) {
            if (!vis[j] && lowcost[j] < Min) {
                Min = lowcost[j];
                t = j;
            }
        }
        if (Min == INF) {
            break;
        }
        sum += Min;
        vis[t] = true;
        for (int j = 2; j <= N; ++j) {
            if (!vis[j] && lowcost[j] > G[t][j]) {
                lowcost[j] = G[t][j];
            }
        }
    }
    return sum;
}


51Nod贪心的经典算法_Kruskal算法

Kruskal算法的高效实现需要一种称作并查集的结构

我们在这里不介绍并查集,只介绍Kruskal算法的基本思想和证明,实现留在以后讨论

Kruskal算法的过程:

(1) 将全部边按照权值由小到大排序
(2) 按顺序(边权由小到大的顺序)考虑每条边,只要这条边和我们已经选择的边不构成圈,就保留这条边,否则放弃这条边

算法 成功选择(n-1)条边后,形成一个棵最小生成树,当然如果算法无法选择出(n-1)条边,则说明原图不连通


以下图为例:


边排序后为:
1 AF 1
2 DE 4
3 BD 5
4 BC 6
5 CD 10
6 BF 11
7 DF 14
8 AE 16
9 AB 17
10 EF 33


算法处理过程如下:


处理边AF,点A与点F不在同一个集合里,选中AF


处理边DE,点D与点E不在同一个集合里,选中DE


处理边BD,点B与点D不在同一个集合里,选中BD


处理边BC,点B与点C不在同一个集合里,选中BC


处理边CD,点C与点D在同一个集合里,放弃CD

处理边BF,点B与点F不在同一个集合里,选中BF

至此,所有的点都连在了一起,剩下的边DF,AE,AB,EF不用继续处理了,算法执行结束


Kruskal算法的证明

假设图连通,我们证明Krusal算法得到一棵最小生成树

我们假设Kruskal算法得到的树是K (注意我们已经假设Kruskal算法一定可以得到生成树)

假设T是一棵最小生成树,并且K ≠T, K中显然至少有一条边。我们找到在K中,而不在T中最小权值的边e


把e加入T中,则形成一个圈,删掉这个圈中不在K中的边f,得到新的生成树T’
f的存在性,如果全里面所有的边都在K中,则K包含圈,矛盾

考虑边权值关系:

(1) 若w(f) > w(e), 则T’的权值和小于T的权值和,与T是最小生成树矛盾
(2) 若w(f) < w(e), 说明Kruskal算法在考虑加入e之前先考虑了边f
之所以没加入f是因为f和之前加入的边形成圈,之前加入的边权值显然不超过w(f) (因为加边是从小到大的顺序加入的)
所以之前加入的边权值一定小于w(e)
而根据e的定义,K中权值小于w(e)的边都在T中,这说明T中的边会和f构成圈,矛盾

所以只能w(f) = w(e)

T’仍然是最小生成树,而T’和K相同的边多了一条
这样下去有限步之后,最终可以把T变为K,从而K也是最小生成树


#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <queue>
#include <vector>
#include <stack>

using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef unsigned int uint;

const int INF = 0x3f3f3f3f;
const int maxn = 50000 + 10;
int pre[1010];
int N;

struct Node {
    int S, E, W;
};

Node node[maxn];

int cmp_num(const void* a, const void* b);
int Find_Root(int x);

int main()
{
#ifdef __AiR_H
    freopen("in.txt", "r", stdin);
#endif // __AiR_H
    int M;
    scanf("%d%d", &N, &M);
    for (int i = 1; i <= N; ++i) {
        pre[i] = i;
    }
    for (int i = 0; i < M; ++i) {
        scanf("%d%d%d", &node[i].S, &node[i].E, &node[i].W);
    }
    qsort(node, M, sizeof(node[0]), cmp_num);
    int ans = 0;
    for (int i = 0; i < M; ++i) {
        int fx = Find_Root(node[i].S);
        int fy = Find_Root(node[i].E);
        if (fx != fy) {
            pre[fx] = fy;
            ans += node[i].W;
        }
    }
    printf("%d\n", ans);
    return 0;
}

int cmp_num(const void* a, const void* b)
{
    Node* x = (Node*)a;
    Node* y = (Node*)b;
    return (x->W - y->W) > 0 ? 1 : -1;
}

int Find_Root(int x)
{
    int r = x;
    while (pre[r] != r) {
        r = pre[r];
    }
    while (x != r) {
        int t = pre[x];
        pre[x] = r;
        x = t;
    }
    return r;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值