【信息学奥赛一本通 提高组】第一章 贪心算法

一、贪心算法的特点:

1、贪心选择:

  所谓贪心选择是指应用同一规则,将原问题变为一个相似的但规模更小的子问题,而后的每一步都是当前看似最佳的选择,且这种选择只依赖于已做出的选择,不依赖未做出的选择。

2、最优子结构:

  执行算法时,每一次得到的结果虽然都是当前问题的最优解(即局部最优解),但只有满足全局最优解包含局部最优解是,才能保证最终得到的结果是最优解。

 

二、几个贪心例子:

1、最优装载问题

  给n个物品,第i个物品重量为wi,选择尽量多的物品,使得总重量不超过C。

【思路】

  由于只关心物品的数量,所以装重的没有装轻的划算,只需把所有物品按重量从小到大排序,以此选择每个物品,知道装不下为止。它只顾眼前,却能得到全局最优解。

 

2、部分背包问题

  有n个物品,第i个物品的重量为wi,价值为vi,在总重量不超过C的情况下,让总价值尽量高,每一个物品可以只取走一部分,价值和重量按比例计算。

【思路】

  贪心策略:先选出性价比最高的。

  注意:由于每个物品可以只选出一部分,因此一定可以让总重量恰好为C(或者所有物品全选,总重量还不足C),而且除了最后一个以外,所有物品要么不选,要么全部选。

 

3、乘船问题

  有n个人,第i个人重量为wi。每艘船的载重量均为C,最多可乘两个人。求用最少的船装在所有人的方案。

【思路】

  贪心策略:最轻的人和最重的人配对。

  程序实现:我们只需用两个指针i和j分别表示当前考虑的最轻的人和最重的人。每次先将指针j往左移动,直到i和j可以共乘一艘船,然后将指针i往右移,指针j往左移动。

 

三、贪心算法的经典应用

1、选择不相交区间问题

  给定n个开区间(ai,bi),选择尽量多个区间,使得这些区间两两没有公共点。

【思路】

  首先,按照结束时间 b1 <= b2 …… <= bn 的顺序排序,依次考虑各个活动,如果没有和已经选择的活动冲突,就选,否则就不选。

【正确性】

  假设 bj < bi 且(aj,bj) 、(ai,bi) 分别与之前的活动不冲突,当前选(aj,bj),若(aj,bj)与(ai,bi)不冲突,则还可以选择(ai,bi),答案个数加一,若(aj,bj)与(ai,bi) 冲突,因为 bj < bi ,所以 (aj,bj)对以后的影响更小。

 

【例题】:活动安排

题目链接:传送门

 

题目描述

设有n个活动的集合E={1,2,..,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi。如果选择了活动i,则它在时间区间[si,fi)内占用资源。若区间[si,fi)与区间[sj,fj)不相交,则称活动i与活动j是相容的。也就是说,当fi≤sj或fj≤si时,活动i与活动j相容。选择出由互相兼容的活动组成的最大集合。

输入

第一行一个整数n(1≤n≤1000);
接下来的n行,每行两个整数si和fi。

输出

输出互相兼容的最大活动个数。

 

样例输入

4
1 3
4 6
2 5
1 7

样例输出

2

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N = 10005;
 4 typedef struct Node {
 5     int S,F;
 6 }Node ;
 7 Node a[N];
 8 bool cmp ( Node u , Node v ){
 9     if( u.F == v.F ){
10         return u.S < v.S ;
11     }
12     return u.F < v.F ;
13 }
14 int n ;
15 int main () {
16     scanf("%d",&n);
17     for ( int i = 0 ;  i < n ; i++ ){
18         scanf("%d%d",&a[i].S, &a[i].F );
19     }
20     sort ( a , a+n ,cmp );
21     int ans = 1 ;
22     int F = a[0].F;
23     for ( int i = 1 ; i < n ; i++ ){
24         if( F <= a[i].S ){
25             F = a[i].F ;
26             ans ++ ;
27         }
28     }
29     printf("%d\n",ans );
30     return 0;
31 }
活动安排

 


 

2、区间选点问题  

  给定n个闭区间[ai,bi],在数轴上选尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)

【思路】

  贪心策略:取最后一个

  首先按区间的结束位置从小到大排序。然后从区间1到区间n进行选择;对于当前区间,若集合中的数不能覆盖它,则将区间末尾的数加入集合。

 

【例题】 种树

题目描述

   一条街的一边有几座房子。因为环保原因居民想要在路边种些树。路边的地区被分割成块,并被编号成1..N。每个部分为一个单位尺寸大小并最多可种一棵树。每个居民想在门前种些树并指定了三个号码B,E,T。这三个数表示该居民想在B和E之间最少种T棵树。当然,B≤E,居民必须记住在指定区不能种多于区域地块数的树,所以T≤E-B+l。居民们想种树的各自区域可以交叉。你的任务是求出能满足所有要求的最少的树的数量。

  写一个程序计算最少要种树的数量。

输入

第一行包含数据N,区域的个数(0<N≤30000);
第二行包含H,房子的数目(0<H≤5000);
下面的H行描述居民们的需要:B E T,0<B≤E≤30000,T≤E-B+1。

输出

树的数目。

样例输入

9
4
1 4 2
4 6 2
8 9 2
3 5 2

样例输出

5


 【题解】:

种树要种得少,就要使一棵树给多个区间使用,这样,尽量在重叠区间种树即可,而重叠位置一定是在区间尾部。处理问题时,先按所有区间的结束位置排序,之后依次处理每个区间,先在第一个区间尾部种满足要求的树,对下一个区间,看差多少颗就在该区间尾部种多少。

 

【算法步骤】:

1、先按结束位置快速排序。

2、对每个区间依次处理。

  a、从前往后扫描这个区间,统计已选点的个数

  b、若已选点的个数超过了要求的点数,则continue

  c、否则从该区间由后向前扫描,添加缺少的覆盖点。

3、输出ans

 

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N = 3e5+100;
 4 typedef struct Node {
 5     int b,e,t;
 6     bool operator < (const Node &rhs) const {
 7         if( e == rhs.e ){
 8             return b < rhs.b;
 9         }
10         return e < rhs.e ;
11     }
12 }Node ;
13 Node a[N];
14 int used[N];
15 int n , h ;
16 int main ()
17 {
18     scanf("%d%d",&h,&n);
19     for(int i=0;i<n;i++){
20         scanf("%d%d%d",&a[i].b,&a[i].e,&a[i].t);
21     }
22     sort ( a , a+n );
23     for(int i=0;i<n;i++){
24         int k = 0 ;
25         for(int j=a[i].b ; j<=a[i].e;j++){
26             if ( used[j] ){
27                 k ++ ;
28             }
29         }
30         if( k >= a[i].t ){
31             continue;
32         }
33         for(int j=a[i].e ; j>=a[i].b ; j--){
34             if( used[j] == 0 ){
35                 used[j] = 1 ;
36                 k++;
37                 if( k==a[i].t)break;
38             }
39         }
40     }
41     int ans = 0 ;
42     for(int i=1;i<=h;i++){
43         if( used[i] )
44             ans ++ ;
45     }
46     printf("%d\n",ans);
47     return 0;
48 }
种树


3、区间覆盖问题

  给n个闭区间 [ ai  , bi ] ,选择尽量少的区覆盖一条指定的线段区间  [s,t] 

【思路】
  贪心策略:在某个时刻的s,找一个满足  a[i]<=s  的  b[i]  最大值即可

  将所有的区间按左端点从小到大排序,依次处理每个区间。每次选择覆盖点s的区间中右端点坐标最大的一个,并将s更新为该区间的右端点坐标,知道选择的区间已包含了t为止。

 

【例题3】喷水装置

题目描述

  长L米,宽W米的草坪里装有n个浇灌喷头。每个喷头都装在草坪中心线上(离两边各W/2米)。我们知道每个喷头的位置(离草坪中心线左端的距离),以及它能覆盖到的浇灌范围。
  请问:如果要同时浇灌整块草坪,最少需要打开多少个喷头?
                                                

输入

输入包含若干组测试数据。
第一行一个整数T表示数据组数;
每组数据的第一行是整数n、L和W(n≤15000);
接下来的n行,每行包含两个整数,给出一个喷头的位置和浇灌半径(上面的示意图是样例输入第一组数据所描述的情况)。

输出

对每组测试数据输出一个数字,表示要浇灌整块草坪所需喷头数目的最小值。如果所有喷头都打开也不能浇灌整块草坪,则输出 -1

样例输入

3
8 20 2
5 3
4 1
1 2
7 2
10 2
13 3
16 2
19 4
3 10 1
3 5
9 3
6 1
3 10 1
5 3
1 1
9 1

样例输出

6
2
-1

【题解】:

1、读入数据,并计算S[i].a = x - sqrt ( r*r - (w*w)/4 )

           S[i].b = x + sqrt( r*r - (w*w)/4 )

2、按S[i].a 进行从小到大快排

3、从左往右依次处理每个区间 

 

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N = 1e5+10;
 4 typedef struct Node {
 5     double s,t;
 6     bool operator < ( const Node &rhs ) const {
 7         return s < rhs.s ;
 8     }
 9 }Node;
10 Node a[N];
11 int main()
12 {
13     int T ,n , L ;
14     double W;
15     scanf("%d",&T);
16     while(T--){
17         scanf("%d%d%lf",&n,&L,&W);
18         int cnt = 0 ;
19         for(int i=0,x,r;i<n;i++){
20             scanf("%d%d",&x,&r);
21             if( r <= W/2 ) continue;
22             a[cnt].s = x - sqrt( r*r - W/2 * W/2 );
23             a[cnt].t = x + sqrt( r*r - W/2 * W/2 );
24             cnt ++ ;
25         }
26         sort(a,a+cnt);
27         double t = 0 ;
28         int ans = 0 , flag = 1 , i = 0 ;
29         while ( t < L ){
30             ans ++ ;
31             double s = t ;
32             for ( ; a[i].s <= s && i < cnt ;i++ ){
33                 if( t < a[i].t ) t = a[i].t ;
34             }
35             if ( t==s && s<L ){
36                 printf("-1\n");
37                 flag = 0 ;
38                 break;
39             }
40         }
41         if ( flag ){
42             printf("%d\n",ans);
43         }
44     }
45     return 0;
46 }
喷水装置

 


4、流水作业调度问题

【问题描述】

  有n个作业要在两台机器M1和M2组成的流水线上完成加工。每个作业i都必须先花时间ai在M1上加工,然后花时间bi在M2上加工。

  确定n个作业的加工顺序,使得从作业1在机器M1上加工开始到作业n在机器M2上加工为止所用的总时间最短。

【思路】:

  直观上,最优调度一定让M1没有空闲,M2的空闲时间尽量短。

  Johnson算法:设N1为a<b的作业集合,N2为a>=b的作业集合,将N1的作业按a非减序排序,N2中的作业按照b的非增序排序,则N1作业接N2作业构成最优排序。

  算法的程序易实现,时间复杂度为O(nlogn),正确性需要证明.

 

【例题4】加工生产调度

题目描述

有n个部件需在A、B机器上加工,每个工件都必须经过先A后B两道工序。
已知:部件i在A、B机器上的加工时间分别为ai,bi。
问:如何安排n个工件的加工顺序,才能使得总加工时间最短?

 

输入

第1行仅一个整数n (0<n<1000),表示产品的数量;
第2行n个整数,表示这n个产品在A车间加工各自所要的时间(都是整数);
第3行n个整数,表示这n个产品在B车间加工各自所要的时间(都是整数)。

 

输出

只有一个数,表示最少的加工时间;

 

样例输入

5
3 5 8 7 10
6 2 1 4 9

样例输出

34

 

【贪心策略】

  要使机器总的空闲时间最短,就要把在A机器上加工时间最短的部件最先加工,这样使得B机器能在最短的空闲时间内开始加工;把在B机器上加工时间最短的部件放在最后加工,这样使得A机器用最短时间等待B机器完工。

 

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N = 1e4+100;
 4 typedef struct Node {
 5     int a,b,id;
 6 }Node ;
 7 Node N1[N],N2[N];
 8 int a[N],b[N],ans[N],n;
 9 bool cmp1 ( Node u,Node v ){
10     return u.a < v.a ;
11 }
12 bool cmp2 ( Node u,Node v ){
13     return u.b > v.b ;
14 }
15 int main()
16 {
17     scanf("%d",&n);
18     int cnt1 = 0 , cnt2 = 0 ;
19     for(int i=0;i<n;i++){
20         scanf("%d",&a[i]);
21     }
22     for(int i=0;i<n;i++){
23         scanf("%d",&b[i]);
24         if( a[i] <= b[i] ){
25             N1[cnt1].a = a[i] ;
26             N1[cnt1].b = b[i] ;
27             N1[cnt1].id = i+1 ;
28             cnt1 ++ ;
29         }else{
30             N2[cnt2].a = a[i] ;
31             N2[cnt2].b = b[i] ;
32             N2[cnt2].id = i+1 ;
33             cnt2 ++ ;
34         }
35     }
36     sort ( N1 , N1 + cnt1 , cmp1 );
37     sort ( N2 , N2 + cnt2 , cmp2 );
38     int A = 0 , B = 0 , cnt = 0 ;
39     for( int i = 0 ; i < cnt1 ; i++ ){
40         ans[cnt++] = N1[i].id;
41     }
42     for( int i = 0 ; i < cnt2 ; i++ ){
43         ans[cnt++] = N2[i].id;
44     }
45     for( int i = 0 ; i < cnt1 ; i++ ){
46         A += N1[i].a;
47         if ( A > B ) B = A ;
48         B += N1[i].b;
49     }
50     for( int i = 0 ; i < cnt2 ; i++ ){
51         A += N2[i].a;
52         if ( A > B ) B = A ;
53         B += N2[i].b;
54     }
55     printf("%d\n",B);
56 
57     for(int i=0;i<cnt;i++){
58         printf("%d%c",ans[i],i==cnt-1?'\n':' ');
59     }
60     return 0;
61 }
加工生产调度

 


 

 

5、带限期和罚款的单位时间任务调度

【问题描述】  

  有n个任务,每个都需要1个时间单位执行,任务i的截止时间di ( 1<= di <= n ) 表示要求任务i在时间di结束时必须完成,误时惩罚wi表示若任务i未在时间di结束之前完成,将导致wi的罚款。

 

【思路】

  要使罚款最少,我们显然应尽量完成w[i]值较大的任务。

  此时,我们可以将任务按w[i]从大到小进行排序,然后按照排好的顺序依次对任务进行安排。安排的规则为:使处理任务i的时间既在d[i]之内,又尽量靠后;如果d[i]之内的时间都已经排满,就放弃处理此项任务。

 

【实现方法】

  1、先按照罚款数额从大到小快排。

  2、顺序处理每个任务,若能安排,则找一个最晚时间;否则放在最后的空位上。

 

【例题5】智力大冲浪

Problem description

  小伟报名参加中央电视台的智力大冲浪节目。本次挑战赛吸引了众多参赛者,主持人为了表彰大家的勇气,先奖励每个参赛者m元。先不要太高兴!因为这些钱还不一定都是你的?!接下来主持人宣布了比赛规则:
  首先,比赛时间分为n个时段(n≤500),它又给出了很多小游戏,每个小游戏都必须在规定期限ti前完成(1≤ti≤n)。如果一个游戏没能在规定期限前完成,则要从奖励费m元中扣去一部分钱wi,wi为自然数,不同的游戏扣去的钱是不一样的。当然,每个游戏本身都很简单,保证每个参赛者都能在一个时段内完成,而且都必须从整时段开始。主持人只是想考考每个参赛者如何安排组织自己做游戏的顺序。作为参赛者,小伟很想赢得冠军,当然更想赢取最多的钱!注意:比赛绝对不会让参赛者赔钱!

Input format 

  共4行。
  第1行为m,表示一开始奖励给每位参赛者的钱;
  第2行为n,表示有n个小游戏;
  第3行有n个数,分别表示游戏1到n的规定完成期限;
  第4行有n个数,分别表示游戏1到n不能在规定期限前完成的扣款数。

Output format

  仅1行。表示小伟能赢取最多的钱。

Problem analysis

  不难想到尽量把每一个活动都放在结束前一秒完成,为其他活动争取更大的空间

  这只是一个理想的情况,有些活动不得不放弃,所以要优先完成扣款多的游戏

  既然如此,把扣款多的游戏从多到少都尽量放在结束前1秒,把玩游戏的时间标记,如果这个时间已经被占用,就往前移,直到无法放置,这个贪心法则绝对是正确的,因为每个游戏最多占用1秒,一旦会导致后面某些游戏玩不了,那么即使不玩这个游戏,后面的游戏也最多只能玩1个,且扣更多的款

 

  得证贪心法则:按游戏扣款从多到少按时间从后往前完成

Source code

  

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N = 1e5+100;
 4 typedef struct Node{
 5     int d,w;
 6     bool operator < ( const Node &rhs ) const {
 7         return w > rhs.w ;
 8     }
 9 }Node;
10 int n,m;
11 Node a[N];
12 int vis[N];
13 int main()
14 {
15     scanf("%d%d",&m,&n) ;
16     for ( int i = 0 ; i < n ; i++ ){
17         scanf("%d",&a[i].d);
18     }
19     for ( int i = 0 ; i < n ; i++ ){
20         scanf("%d",&a[i].w);
21     }
22     sort ( a , a+n );
23     for ( int i = 0 ; i < n ; i++ ){
24         int f = 0 ;
25         for(int j = a[i].d ; j >= 1 ; j -- ){
26             if( vis[j] == 0 ){
27                 f = 1;
28                 vis[j] = 1 ;
29                 break;
30             }
31         }
32         if( !f ){
33             //printf("(%d,%d)\n",a[i].d,a[i].w);
34             for ( int j = n ; j >= 1 ; j-- ){
35                 if( vis[j] == 0 ){
36                     vis[j] = 1 ;
37                     break;
38                 }
39             }
40             m = m - a[i].w ;
41         }
42     }
43     printf("%d\n",m);
44     return 0;
45 }
智力大冲浪

 


 

【习题1】数列极差

题目描述

  “我就说你在忽悠我吧,我刚才问了昆士兰大学的好多魔法师,他们根本就没有一个人想过做什么时间旅行的实验,但搞笑的是,他们居然对你的理论很感兴趣,想找个时间和你做进一步的讨论。哎,我还以为他们和我一样聪明呢,想不到这么容易上当受骗。”小墨老师摆出一幅你骗不了我的表情。

  “唉,你太自以为是了,这样吧,你先把这道数列极差问题发给他们,如果他们能有所领悟,那我会找时间和他们讨论一下时间旅行的可行性的。”李旭琳边说边在黑板上写了N个正整数组成的一个数列,并进行如下操作:每次擦去其中的两个数a和b,然后在数列中加入一个数a×b+1,如此下去直至黑板上剩下一个数,在所有按这种操作方式最后得到的数中,最大的为max,最小的为min,则该数列的极差定义为M=max-min。

  现在请你编程,对于给定的数列,计算极差。

输入

输入包含多个测试集。每个测试集的第一个数N表示 正整数序列长度(0≤N≤50000),随后是N个正整数。N为0表示输入结束。

输出

每个结果一行。

 

样例输入

3
1 2 3
0

样例输出

2

提示

每次合并最小的两个最后得到的是最大的,而每次合并最大的两个的时候最后得到的是最小的。
例如有6个数,即8 6 5 9 7 1
则min求值过程为:    max求值过程为:
73  7  6  5  1     6  6  7  8  9
512  6  5  1       37  7  8  9
3073  5  1        37  57  9
15366  1          334  57
15367             19039
ans=19039-15367=3672

 

【贪心策略】:

  从提示中可以看出来,这个题目就是考察在数列中找两个最大或者最小的进行相乘得到的值放回去,依次类推,知道数列中只剩下一个数为止。

【题解】:

  我们用到的是优先队列来处理这个问题。只要每次从优先队列里拿出头两个进行相乘,然后返回去,直到队列中剩下一个数为止。

【代码】:

   

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N = 1e5+10;
 4 typedef long long ll;
 5 ll a[N] ;
 6 int n ;
 7 int cmp ( ll a, ll b ){
 8     return a > b ;
 9 }
10 int main()
11 {
12     int n;
13     while(~scanf("%d",&n),n){
14         priority_queue < ll > Q1 ;
15         priority_queue < ll , vector<ll> , greater<ll> > Q2;
16         for(int i=0;i<n;i++){
17             scanf("%lld",&a[i]);
18             Q1.push(a[i]);
19             Q2.push(a[i]);
20         }
21 
22         ll Minz = Q1.top();
23         //Q1.pop();
24         while( !Q1.empty() ){
25             ll t = Q1.top();
26             Q1.pop();
27             Minz = t ;
28             if( Q1.empty() ) break;
29             ll t2 = Q1.top();
30             Q1.pop();
31             ll tmp = t * t2 + 1;
32             Q1.push(tmp);
33         }
34 
35         ll Maxz = Q2.top();
36         //Q2.pop();
37         while(!Q2.empty()){
38             ll t = Q2.top();
39             Q2.pop();
40             Maxz = t ;
41             if( Q2.empty() ) break;
42             ll t2 = Q2.top();
43             Q2.pop();
44             ll tmp = t * t2 + 1;
45             Q2.push(tmp);
46         }
47 
48         ll ans = Maxz - Minz ;
49         printf("%lld\n",ans);
50     }
51 
52     return 0;
53 }
数列极差

 


 

 

【习题2】数列分段

题目描述

对于给定的一个长度为N的正整数数列Ai,现要将其分成连续的若干段,并且每段和不超过M(可以等于M),问最少能将其分成多少段使得满足要求。

输入

第一行包含两个正整数N,M,表示了数列Ai的长度与每段和的最大值;
第二行包含N个空格隔开的非负整数Ai。

输出

仅包含一个正整数,输出最少划分的段数。

样例输入

5 6
4 2 4 5 1

样例输出

3

提示

对于20%的数据,有N≤10;
对于40%的数据,有N≤1000;
对于100%的数据,有N≤105,M≤109,M大于所有数的最大值,Ai之和不超过109。

 

【题解】:

  这个贪心策略非常明显,因为只要加到一定数就可以清空另起一段了,但是这个时候需要有一个小细节处理,因为这一段之和可能是 (等于m )或者是 (大于m),

这两种情况对于新增的一段是不一样的,所以要分开讨论。

  1、等于m时,下一段  为  0 ,ans++

  2、大于m是,下一段  为 当前值,ans++

最后如果结束时也要多加一个判断,因为可能最后一次不为0,所以要新增一段。具体可以看代码;

 

【代码】:

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N = 1e6+100;
 4 int a[N];
 5 int main()
 6 {
 7     int n,m,cur=0,ans = 0 ;
 8     cin >> n >> m ;
 9     for(int i=1;i<=n;i++) cin >> a[i] ;
10     for(int i=1;i<=n;i++){
11         cur = cur + a[i] ;
12         if( cur > m ){
13             cur = a[i] ;
14             ans ++ ;
15             //cout << "### "<< cur << endl;
16         }else if( cur == m ){
17             cur = 0 ;
18             //cout << "@@@ "<< cur << endl;
19             ans ++ ;
20         }
21     }
22     if( cur != 0  ){
23         ans ++ ;
24     }
25     cout << ans << endl ;
26     return 0;
27 }
数列分段

 

 


 【习题3】线段

题目描述

  数轴上有n条线段,选取其中k条线段使得这k条线段两两没有重合部分,问k最大为多少。

输入

  第一行为一个正整数n;
  在接下来的n行中,每行有2个数ai,bi,描述每条线段。

输出

  输出一个整数,为k的最大值。

 

样例输入

3
0 2
2 4
1 3

样例输出

2

 

提示

对于20%的数据,n≤10;
对于50%的数据,n≤103;
对于70%的数据,n≤105;
对于100%的数据,n≤106,0≤ai<bi≤106。


 

【题解】:

  其实这个题目就是例题1,活动安排,一模一样,直接复制粘贴交上就过了。

【贪心策略】:

  建立结构体,然后按照右端点排序,先结束的活动排在前面,这样局部最优就可以扩展全局最优

【代码】:

  

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N = 1e6+10;
 4 typedef struct Node
 5 {
 6     int S,F;
 7 } Node ;
 8 Node a[N];
 9 bool cmp ( Node u, Node v )
10 {
11     if( u.F == v.F )
12     {
13         return u.S < v.S ;
14     }
15     return u.F < v.F ;
16 }
17 int n ;
18 int main ()
19 {
20     scanf("%d",&n);
21     for ( int i = 0 ;  i < n ; i++ )
22     {
23         scanf("%d%d",&a[i].S, &a[i].F );
24     }
25     sort ( a, a+n,cmp );
26     int ans = 1 ;
27     int F = a[0].F;
28     for ( int i = 1 ; i < n ; i++ )
29     {
30         if( F <= a[i].S )
31         {
32             F = a[i].F ;
33             ans ++ ;
34         }
35     }
36     printf("%d\n",ans );
37     return 0;
38 }
线段

 

【习题4】家庭作业

题目描述

老师在开学第一天就把所有作业都布置了,每个作业如果在规定的时间内交上来的话才有学分。每个作业的截止日期和学分可能是不同的。例如如果一个作业学分为10,要求在6天内交,那么要想拿到这10学分,就必须在第6天结束前交。
每个作业的完成时间都是只有一天。例如,假设有7次作业的学分和完成时间如下:

         

老师在开学第一天就把所有作业都布置了,每个作业如果在规定的时间内交上来的话才有学分。每个作业的截止日期和学分可能是不同的。例如如果一个作业学分为10,要求在6天内交,那么要想拿到这10学分,就必须在第6天结束前交。
每个作业的完成时间都是只有一天。例如,假设有7次作业的学分和完成时间如下:

 

输入

第一行一个整数N,表示作业的数量;
接下来N行,每行包括两个整数,第一个整数表示作业的完成期限,第二个数表示该作业的学分。

 

输出

输出一个整数表示可以获得的最大学分。保证答案不超过C/C++的int范围。

 

样例输入

7
1 6
1 7
3 2
3 1
2 4
2 5
6 1

样例输出

15

提示

对于20%的数据,N≤103;
对于40%的数据,N≤104;
对于60%的数据,N≤105;
对于100%的数据,N≤1e6,作业的完成期限均小于7×1e5

 


 

【分析】:

  这个题目属于“带限期和罚款的单位时间任务调度”类型的题目;

  然后这个题目不能像例题一样直接扫,因为例题的n仅仅是500,哪怕是O(n^3)都可以接受。

  但是这个题目n=1e6,所以只能接受O(nlogn)的算法。

【题解】:

  其实我想到的是二分来找位置删除的想法,但奈何没有删除最大小于的位置和set配合使用。

  无奈只能上网看看被人的做法,后来看到了龙哥的做法,真的你龙哥还是你龙哥。

  龙哥用并查集的想法模拟链表删除的写法,真令人感叹这是何等厉害的想法。

【步骤】:

  首先需要两样东西,一个数组pre[N],模拟链表链接上一个的下标。

  另一个是Find函数,就是 return pre[x] = Find( pre[x] )递归实现。

【具体代码】:

  

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 typedef long long ll;
 4 const int N = 1e6+100;
 5 typedef struct Node {
 6     int w,d;
 7     bool operator < ( const Node & rhs )const {
 8         return w > rhs.w ;
 9     }
10 }Node;
11 Node a[N];
12 int pre[N];
13 bool vis[N];
14 int Find ( int x ){
15     if ( vis[x] == 0 )
16         return x;
17     return pre[x] = Find(pre[x]);
18 }
19 int main()
20 {
21     int n;
22     scanf("%d",&n);
23     for(int i=0,d,w;i<n;i++){
24         scanf("%d%d",&d,&w);
25         a[i] = Node { w , d };
26         pre[i] = i - 1 ;
27     }
28     sort ( a , a+n );
29     int ans = 0 ;
30     for(int i=0 ; i<n ; i++ ){
31         int f = Find(a[i].d);
32         if( f == 0 ) continue;
33         ans += a[i].w ;
34         vis[f] = 1 ;
35     }
36     printf("%d\n",ans);
37 }
家庭作业

 


 

【习题5】钓鱼

题目描述

  在一条水平路边,有n个钓鱼湖,从左到右编号为1,2,…,n。佳佳有H个小时的空余时间,他希望利用这个时间钓到更多的鱼。他从1出发,向右走,有选择的在一些湖边停留一定的时间(是5分钟的倍数)钓鱼。最后在某一个湖边结束钓鱼。佳佳从第i个湖到第i+1个湖需要走5×Ti分钟路,还测出在第i个湖停留,第一个5分钟可以钓到Fi条鱼,以后每再钓5分钟,可以钓到的鱼量减少Di,若减少后的鱼量小于0,则减少后的鱼量为0。为了简化问题,佳佳假定没有其他人钓鱼,也没有其他因素影响他钓到期望数量的鱼。请编程求出佳佳最多能钓鱼的数量。

 

输入

  第一行一个整数n(2≤n≤100),表示湖的个数
  第二行一个整数H(1≤H≤20),表示佳佳的空闲时间
  第三行有n个整数,依次表示每个湖第一个5分钟能钓到鱼的数量
  第四行有n个整数,依次表示以后的每5分钟钓鱼数量比前一个5分钟钓鱼数量减少的数量
  第五行有n−1个整数,Ti表示由第i个湖到第i+1个湖需要花5×Ti分钟的路程

 

输出

  输出只有一行,表示佳佳最多能钓鱼的数量。

 

样例输入

3
1
4 5 6
1 2 1
1 2

样例输出

35

 

提示

在第1个湖钓15分钟,共钓得4+3+2=9条鱼;
在第2个湖钓10分钟,共钓得5+3=8条鱼;
在第3个湖钓20分钟,共钓得6+5+4+3=18条鱼;
从第1个湖到第2个湖,从第2个湖到第3个湖,共用时间15分钟,共得35条鱼,并且这是最多的数量。

 


 

 

【分析】:

  这个题目是习题里面比较难的题目了,网上的题解有些是动态规划,然而我看了看其实用优先队列还是来得比较好用而且更好理解,其实一开始我也觉得这个题目很难全部分析出来,但是看了一些别人的代码分析一下,其实真的很好理解。

【题解】:

  第一步:首先所有变量进行标准化,所有的时间全部转化为5分钟来计算,譬如一个小时有12个5分钟 ,钓鱼一次使用的第一个五分钟直接减1就行了。

  第二步:a、以第i个池塘来讨论,

      b、到第i个池塘必须要先扣除路程中要花费的5分钟,然后再进行钓鱼,用优先队列存放每一个池塘当前钓鱼的个数,

      如果选择了第 j 个池塘钓鱼后,要更新第 j 个池塘的 钓鱼的数量,压会优先队列中。

  第三步:不断执行第二步(b),直到时间为 0  ,或者优先队列中的所有的钓鱼量<=0 时则跳出,然后更新答案,返回第二步(a)

【代码】:

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N = 110;
 4 int dis[N],n,h;
 5 typedef struct Node {
 6     int Fish , Dec ;
 7     bool operator < ( const Node & rhs ) const {
 8         return Fish < rhs.Fish ;
 9     }
10 }Node ;
11 
12 Node a[N];
13 int main()
14 {
15     scanf("%d%d",&n,&h);
16     for (int i=1;i<=n;i++) scanf("%d",&a[i].Fish);
17     for (int i=1;i<=n;i++) scanf("%d",&a[i].Dec);
18     for (int i=2;i<=n;i++) scanf("%d",&dis[i]);
19 
20     h*=12 ;
21 
22     int sum = 0 , ans = 0 ;
23     for ( int i = 1 ; i <= n ; i++ ){
24         h -= dis[i] ;
25         sum = 0 ;
26         priority_queue < Node > Q ;
27         for ( int j = 1 ; j <= i ; j++ ){
28             Q.push ( a[j] ) ;
29         }
30         int time = h ;
31         while ( time > 0 ){
32             Node cur = Q.top() ;
33             Q.pop() ;
34 
35             if( cur.Fish <= 0 )
36                 break ;
37 
38             sum += cur.Fish ;
39             time -- ;
40 
41             cur.Fish -= cur.Dec;
42             Q.push(cur);
43         }
44         ans = max ( ans , sum ) ;
45     }
46     printf("%d\n",ans);
47     return 0;
48 }
钓鱼

 


 

 

【习题6】糖果传递 

题目描述

  有n个小朋友坐成一圈,每人有ai个糖果。每人只能给左右两人传递糖果。每人每次传递一个糖果代价为1。

输入

  第一行一个正整数nn<=1'000'000,表示小朋友的个数.
  接下来n行,每行一个整数ai,表示第i个小朋友得到的糖果的颗数.

输出

  求使所有人获得均等糖果的最小代价。

样例输入

4
1
2
5
4

样例输出

4


【题解】:   

  这个题目之后我会重新推导一遍,其实这个题目已经不属于贪心范围了,虽然说最后的做法是贪心的想法。

  请大家移步到:洛谷题解区

  里面的神仙分析非常透彻了已经,我就偷懒一下不写证明过程了。

  结论为:排序后找到中位数,然后所有与中位数的绝对值之和就是答案。

【代码】:

  

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 typedef long long ll;
 4 const int N = 1e6+100;
 5 ll a[N],Sum,sum[N];
 6 int main()
 7 {
 8     int n;
 9     scanf("%d",&n);
10     for(int i=1;i<=n;i++){
11         scanf("%lld",&a[i]);
12         Sum = Sum + a[i];
13     }
14     for(int i=1;i<=n;i++){
15         sum[i] = sum[i-1] + a[i] - Sum/n ;
16     }
17     sort ( sum + 1 , sum + 1 + n );
18     int m = (n+1) / 2 ;
19     ll ans = 0 ;
20     for (int i=1;i<=n;i++){
21         ans += abs( sum[i] - sum[m] ) ;
22     }
23     printf("%lld\n",ans );
24     return 0 ;
25 }
糖果传递

 

posted @ 2019-07-09 14:36  Osea  阅读(1643)  评论(0编辑  收藏  举报