蓝桥杯书的笔记(三:接上篇蓝桥云课里的内容,C++)

蓝桥云课的笔记

  • 差分与前缀和算法
    • 差分法
      • 大学里的树木要打药
    • 前缀和算法
      • 大学里的树木要维护
  • 二分查找
    • 分巧克力
    • M 次方根
    • 总结
  • 贪心算法
    • 算法概念:
    • 先看一个启发式问题:找零问题
    • 活动选择型问题之小 B 的宿舍
    • 可拆分背包问题之贪心的自助餐
    • 实验总结

参考博文1(点击)
参考博文2(点击)

差分与前缀和算法

差分与前缀和是一对互逆的操作,常常用于处理区间问题,差分法是解决区间加减问题,前缀和是解决区间求和问题的常用办法。.

差分法

差分法的应用主要是用于处理区间问题。当某一个数组要在很多不确定的区间,加上相同的一个数。我们如果每个都进行加法操作的话,那么复杂度 O(nm) 是平方阶的,非常消耗时间。

如果我们采用差分法,将数组拆分,构造出一个新的拆分数组,通过对数组区间的端点进行加减操作,最后将数组和并就能完成原来的操作。

这样处理后,时间复杂度降低为 O(N),虽然感觉操作变得更加复杂了,但是只用对边界操作确实比操作一整个区间的方法要优秀的多

差分法的特点:

  1. 将对于区间的加减操作转化为对于端点的操作;
  2. 时间复杂度为 O(n);
  3. 用于维护区间的增减但不能维护乘除;
  4. 差分后的序列比原来的数组序列多一个数。

差分算法解题的基本思路:
蓝桥杯书的笔记(三:接上篇蓝桥云课里的内容,C++)_第1张图片

递推算法的一般步骤:

首先假设有一个数组:
1 2 3 4 5 7 2

差分后:
1 1 1 1 1 2 -5 -2

一般应用场景:
让你对区间 [l,r] 加减操作 N 次

如:
从第二个元素到第五个元素每个+3
从第二个元素到第四个元素每个-2
从第一个元素到第三个元素每个+1
....

这里我们先演示前三个:
对于每个 [l,r] 区间的加减操作都转化为对端点 l,r+1 的操作
从第二个元素到第五个元素每个+3:
转化为:[l]+3 并且 [r+1]-3

那么原序列变成了:
1 1 1 1 1 2 -5 -2
1 4 1 1 1 -1 -5 -2

然后我们按照 a[i]=a[i]+a[i-1] 复原:
1 5 6 7 8 7 2 0

去掉最后一项,跟原序列对比:
1 2 3 4 5 7 2
1 5 6 7 8 7 2
确实是都加上了 3。

我们继续操作:
从第二个元素到第四个元素每个-2
转化为:[l]-2 并且 [r+1]+2

那么序列变成了: 
1 4 1 1 1 -1 -5 -2
1 2 1 1 3 -1 -5 -2

然后我们按照a[i]=a[i]+a[i-1] 复原
1 3 4 5 8 7 2 0

与上次复原后对比:
1 5 6 7 8 7 2
1 3 4 5 8 7 2 

确实是按照操作执行了。
注意 Warning:
不用每次都复原,只用最后一次复原即可,这里我是演示给大家看。

我们最后直接做三次,最后还原:
从第二个元素到第五个元素每个+3
从第二个元素到第四个元素每个-2
从第一个元素到第三个元素每个+1

1 2 3 4 5 7 2

原序列差分后:
2 2 1 0 3 -1 -5 -2

2 号元素 + 3 
6 号元素 - 3
2 号元素 - 2
5 号元素 + 2
1 号元素 + 1 
4 号元素 - 1

差分序列变成:
2 2 1 0 3 -1 -5 -2

复原后:
2 4 5 5 8 7 5 0

与原序列对比:
1 2 3 4 5 7 2
2 4 5 5 8 7 5

所以还是非常方便快捷的。

差分与前缀和是逆操作,常在一起出现,但是先做差分还是先做前缀和就是两种不同的算法,做不做另一种操作也决定了算法不同,所以大家要根据题目分析,具体学会使用。

大学里的树木要打药

题意:

教室外有 N 棵树,根据不同的位置和树种,学校要对其上不同的药。

因为树的排列成线性,且非常长,我们可以将它们看作一条直线给他们编号。

树的编号从 0-N-1 且 N<1e6。

对于树的药是成区间分布,比如 3 - 5 号的树靠近下水道,所以他们要用驱蚊虫的药, 20 - 26 号的树,他们排水不好,容易涝所以要给他们用点促进根系的药。

诸如此类,每种不同的药要花不同的钱。

现在已知共有 M 个这样的区间,并且给你每个区间花的钱,请问最后,这些树木花了多少药费。

输入输出:

输入描述:

每组输入的第一行有两个整数 N(1 <= N<= 1000000)和 M(1 <= M <= 100000)。

N 代表马路的共计多少棵树,M代表区间的数目,N 和 M 之间用一个空格隔开。

接下来的 M 行每行包含三个不同的整数,用一个空格隔开,表示一个区域的起始点 L 和终止点 R 的坐标,以及花费。

输入样例:

500 3
150 300 4
100 200 20
470 471 19


输出描述:
输出包括一行,这一行只包含一个整数,所有的花费。

输出样例:
2662

输入样例:
3000 8
150 1130 2
1020 1200 3 
470 2071 1
1123  211 6
12 222 2
13 23 2 
1  213 4
1232  2523 6

输出样例
2662

运行限制:

  1. 最大运行时间:1s
  2. 最大运行内存:128M

蓝桥杯书的笔记(三:接上篇蓝桥云课里的内容,C++)_第2张图片
差分算法解决区间加减问题通用框架如下:

//读入原始数据 n,m,a
输入n,m
for(int i=0;i<n;i++){
    输入a
}

//差分
for(int i=1;i<n;i++)
    a[i]=a[i]-a[i-1] 

//区间操作
while(m--)
{
    输入l,r,value
    a[l]+value
    a[r+1]-value
}

//前缀和还原
for(int i=1;i<n;i++)
    a[i]=a[i]+a[i-1] 

完整代码:

#include 
using namespace std;
int a[100005];
int main()
{
    int n; //n层
    int m; // m个区间
    cin >> n >> m;

    while (m--)
    {
        int l, r, value;
        cin >> l >> r >> value;
        a[l] += value;
        a[r + 1] -= value;
    }

    for (int i = 1; i < n; i++)
        a[i] = a[i] + a[i - 1];

    int sum = 0;

    for (int i = 0; i < n; i++)
        sum += a[i];
    /*
    也可以一次性搞定
    int sum=a[0];
    for(int i=1; i
    cout << sum << endl;
}

前缀和算法

前缀和法的应用主要也是用于处理区间问题。

前缀和是指某序列的前 n 项和,可以把它理解为数学上的数列的前 n 项和。当对于某一数组区间进行多次询问,[L,r] 的和时,如果正常处理,那么我们每次都要 [l,r]。查询 N 次,那么时间复杂度也是 O(nm) 也是平方阶的。

如果我们采用前缀和,构造出一个前缀和数组,通过对于端点的值的减法操作就能 O(1) 的求出 [l,r] 的和。然后 N 次查询的,就将复杂度降低为 O(n)

同差分一样,感觉操作变得更加复杂了,但是只用对端点值的操作确实比一整个区间相加的方法要优秀的多。听到这里大家很期待了,我们接着进行讲解。

蓝桥杯书的笔记(三:接上篇蓝桥云课里的内容,C++)_第3张图片
前缀和的一般解题过程:

首先假设有一个数组:

1 2 3 4 5 7 2

前缀和后:

0 1 3 6 10 15 22 24

一般应用场景:

让你对区间 [l,r] 求和操作N次

如:

从第二个元素到第五个元素的和
从第二个元素到第四个元素的和
从第一个元素到第三个元素的和
....

这里我们先演示前三个:
对于每个 [l,r] 区间的求和操作转化为区间端点的加减操作
sum[l,r] =[r]-[l-1]

从第二个元素到第五个元素的和:
转化为:[5]-[1]

那么Sum[2,5]=[5]-[1]=142+3+4+5=14
确实是相等的,就是这么神奇。

我们继续操作:
从第二个元素到第四个元素的和
转化为:[4]-[1]

那么Sum[2,4]=[4]-[1]=92+3+4=9

我们继续操作:
从第一个元素到第三个元素的和

转化为:[3]-[0]

那么Sum[1,3]=[3]-[0]=61+2+3=6

符合题意,验证结束,咱么做个题目看一看

大学里的树木要维护

题意:

教室外有 N 棵树,根据不同的位置和树种,学校已经对其进行了多年的维护。因为树的排列成线性,且非常长,我们可以将它们看作一条直线给他们编号。

树的编号从 1-N 且 N<1e6。由于已经维护了多年,每一个树都由学校的园艺人员进行了维护费用的统计。

每棵树的前期维护费用各不相同,但是由于未来需要要打药,所以有些树木的维护费用太高的话,就要重新种植。由于维护费用也称区间分布,所以常常需要统一个区间里的树木的维护开销。

现在园艺人员想知道,某个区间内的树木维护开销是多少。共计 M 个区间需要查询。


输入描述:
每组输入的第一行有两个整数 N(1 <= N<= 1000000)和 M(1 <= M <= 100000)。

N 代表马路的共计多少棵树,M 代表区间的数目,N 和 M 之间用一个空格隔开。接下来的一行,包含 N 个数,每个数之间用空格隔开。

接下来的M行每行包含两个不同的整数,用一个空格隔开,表示一个区域的起始点L和终止点R的坐标。

输入样例:
10 3
7 5 6 4 2 5 0 8 5 3
1 5
2 6
3 7

输出描述:
  输出包括M行,每一行只包含一个整数,所有的花费。

输出样例:
24
22
17

输入样例

30 28
172 723 580 822 718 798 941 625 450 716 540 252 16 666 115 679 274 323 875 233 99 538 881 486 610 462 319 878 930 735
6 22
7 21
3 16
7 20
9 17
0 21
13 27
7 19
10 23
2 14
21 22
15 17
6 13
16 23
21 21
11 15
5 12
9 11
8 22
10 16
3 8
15 27
5 16
4 8
0 27
4 8
7 21
20 21

输出样例

8140
6804
7918
6705
3708
10617
6576
6472
6207
7847
637
1068
4338
3902
99
1589
5040
1706
6401
2984
4484
5894
6516
3904
13913
3904
6804
332

运行限制:

  1. 最大运行时间:1s
  2. 最大运行内存:128M

蓝桥杯书的笔记(三:接上篇蓝桥云课里的内容,C++)_第4张图片
这个代码有个问题,虽然是能通过的,但是他是一个输入对应一个输出的,我们之前讲过,这对大部分的测评机是没问题。

#include 
using namespace std;
int a[100005];
int sum[100005];
vector<int> ss;

int main()
{
    int n;
    int m;
    cin >> n >> m;

    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        sum[i] = a[i] + sum[i - 1];
    }

    while (m > 0)
    {
        m -= 1;
        int l, r;
        cin >> l >> r;
        cout << sum[r] - sum[l - 1] << endl;
    }
}

但是如果有想要规规矩矩的处理,或者说题目要求必须全部读入后输出。我们可这样操作。


#include
using namespace std;
int a[100005];
int sum[100005];
vector<int>ss;
int main()
{

    int n ;
    int m;
    cin>>n>>m;

    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        sum[i]=a[i]+sum[i-1];

    }

    while(m>0)
    {
        m-=1;
        int l,r;
        cin>>l>>r;
        ss.push_back(sum[r]-sum[l-1]);
    }

    for(auto sss:ss) cout<<sss<<endl;
}

豆瓣:https://movie.douban.com/top250?start=100&filter=

二分查找

在这里插入图片描述
伪代码过程:

Step 1:

假设存在一有序数组:
下标[ 0   1   2   3   4   5   6   7   8    9    10   11   12 ]
数据[ 7   14  18  21  23  29  31  35   38   42   46   49  52 ]
      ↑                                                   ↑
    low=0                                              high=12

                            mid=(low+high)/2
                            mid=(0+12)/2
                            mid=6
                            [mid]=31>14 所以选择左半部分

操作:
    此时令low不变,high=mid-1=5

Step 2:

下标[ 0   1   2   3   4   5   6   7   8    9    10   11   12 ]
数据[ 7   14  18  21  23  29  31  35   38   42   46   49  52 ]
      ↑                   ↑
   low=0                 high=5

            mid=(low+high)/2
            mid=(0+6)/2
            mid=3
            [mid]=21>14 所以选择左半部分

操作:
    此时令low不变,high=mid-1=2

Step 3:

下标[ 0   1   2   3   4   5   6   7   8    9    10   11   12 ]
数据[ 7   14  18  21  23  29  31  35   38   42   46   49  52 ]
      ↑       ↑
   low=0    high=2

            mid=(low+high)/2
            mid=(0+2)/2
            mid=1
            [mid]=14=14  找到答案

操作:
    返回下标   

模板:

// 在单调递增序列a中查找>=x的数中最小的一个(即x或x的后继)
while (low < high)
{
    int mid = (low + high) / 2;
    if (a[mid] >= x)
        high = mid;

    else
        low = mid + 1;
}

// 在单调递增序列a中查找<=x的数中最大的一个(即x或x的前驱)
while (low < high)
{
    int mid = (low + high + 1) / 2;

    if (a[mid] <= x)
        low = mid;

    else
        high = mid - 1;
}

此处我们先分整数的二分查找法的常用模版,关于实数的部分,我们后面再讲。

下面可能会有同学会疑问道:为什么采用这一套代码的而不是采用查找等于的 X?

是因为这样的适用范围更广,当有 X 时这套代码就返回 X 的位置。如果没有 X,就返回 <=x 的数中最大的一个或者 >=x 的数中最小的一个。

分巧克力

题意:

儿童节那天有 K 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。小明一共有 N 块巧克力,其中第 i 块是 Hi×Wi 的方格组成的长方形。为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。切出的巧克力需要满足:

1. 形状是正方形,边长是整数;
2. 大小相同;

例如一块 6x5 的巧克力可以切出 62x2 的巧克力或者 23x3 的巧克力。

当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?..输入描述:

第一行包含两个整数 N,K (1≤N,K≤1e5)。

以下 N 行每行包含两个整数 Hi Wi (1≤Hi,Wi≤1e5)。

输入保证每位小朋友至少能获得一块 1x1 的巧克力。

输出描述:
输出切出的正方形巧克力最大可能的边长。

样例:

输入样例
2 10
6 5
5 6

输出样例
2

运行限制:
最大运行时间:1s
最大运行内存:256M

注意:
1. 请严格按要求输出,不要画蛇添足地打印类似:“请您输入…”的多余内容。
2. 不要调用依赖于编译环境或操作系统的特殊函数。
3. 所有依赖的函数必须明确地在源文件中
4. 不能通过工程设置而省略常用头文件。

蓝桥杯书的笔记(三:接上篇蓝桥云课里的内容,C++)_第5张图片
代码:

#include

using namespace std;
const int MAXN=100010;
int n,k;
int h[MAXN],w[MAXN];

bool pd(int l)
{
    int sum=0;
    for(int i=0; i<n; i++)
    {
        sum+=(h[i]/l)*(w[i]/l);
        if(sum>=k)
        {
            return true;
        }
    }
    return false;
}

int main()
{
    cin>>n>>k;
    for(int i=0; i<n; i++)
        cin>>h[i]>>w[i];

    //找到二分查找的上界
    int high=0;

    for(int i=0; i<n; i++)
    {
        high=max(high,h[i]);
        high=max(high,w[i]);
    }

    // 二分下届由题意可得至少为1
    int low=1;


    // 由于本题目就是求符合要求的Mid 值所以要将mid定义在二分查找外边

    int mid=0;

    while(low<high)
    {

        mid = (low + high+1) / 2;

        if(pd(mid))
            low=mid;

        else
            high = mid - 1;

//        cout<

    }
    
    //因为low=high所以输出哪一个都一样 
        
    cout<<low;
    return 0;
}

M 次方根

题意:

小A最近在学高等数学,他发现了一道题,求三次根号下27。现在已知,小 A 开始计算,1 的三次方得12 的三次方得 83 的三次方得 27,然后他很高兴的填上了 3。

接着他要求 5 次根号下 164。然后他开始 1 的三次方得 12 的三次方得 83 的三次方得27...

直到他算到了秃头,也没有找到答案。

这时一旁的小 B 看不下去了,说这题答案又不是个整数。小 A 震惊,原来如此。作为程序高手的小 A,打算设计一个程序用于求解 M 次跟下N的值。

但是由于要考虑精度范围,答案必须要保留 7 位小数,连三次根号下 27 都要掰手指的小 A 又怎么会设计呢。请你帮小 A 设计一个程序用于求解 M 次根号 N。

数据范围:
1<= N <= 1e5 
1<= M <= 100
且 M<N

输入描述:

第一行输入整数 N 和 M,数据间用空格隔开。

输出描述:
输出一个整数,并保留 7 位小数。

输入样例:
27 3

输出样例:
3.000000

最大运行时间:1s
最大运行内存: 256M

注意:
1. 请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
2. 不要调用依赖于编译环境或操作系统的特殊函数。
3. 所有依赖的函数必须明确地在源文件中。
4. 不能通过工程设置而省略常用头文件。

题目分析
前面讲的都是整数二分,其实二分法还是可以用于实数。这个题目比较难,很多同学可能想不明白,想不明白就多读题,写写画画理解一下。这个题还有很多解法,现在告诉你了这道理用二分可以解答,请设计一个二分程序。

首先是这道题我们怎么下手:

根据前面的知识,我们要找到一个具有单调性的数列,去二分。这个题的关键是我们要去二分什么,这里可以二分的是 a^M 中的 a,所以我们要先想办法设计出用于处理实数二分的代码。

两个模板:

//模版一:实数域二分,设置eps法

//令 eps 为小于题目精度一个数即可。比如题目说保留4位小数,0.0001 这种的。那么 eps 就可以设置为五位小数的任意一个数 0.00001- 0.00009 等等都可以。

//一般为了保证精度我们选取精度/100 的那个小数,即设置 eps= 0.0001/100 =1e-6

while (l + eps < r)
{
    double mid = (l + r) / 2;

    if (pd(mid))
        r = mid;
    else
        l = mid;
}

//模版二:实数域二分,规定循环次数法

//通过循环一定次数达到精度要求,这个一般 log2N < 精度即可。N 为循环次数,在不超过时间复杂度的情况下,可以选择给 N 乘一个系数使得精度更高。

    for (int i = 0; i < 100; i++)
{

    double mid = (l + r) / 2;
    if (pd(mid))
        r = mid;
    else
        l = mid;
}

模板讲完了,然后我们就要考虑判定条件了,怎样判定是否存在满足大于平均值的区间。当然这个题你可以使用语言中自带开方软件,但是我们还是联系一下实数的二分代码。

关于判定条件,我们应该设计一个代码用于比较 a^m 和 N 的大小关系。

完整代码:

#include 
#include 
#include //用于浮点数输出
using namespace std;

double n,l,r,mid;
double eps=1e-8;

bool pd(double a,int m)//pd 成功的情况,
//一定是 pd 的 mid 符合条件,
//且小于 mid 的一定符合条件。
//因此我们要在大于 mid 中继续查找,找到更大的 mid。
{
    double c=1;

    while(m>0)
    {
        c=c*a;
        m--;
    }

    if(c>=n)
        return true;
    else
        return false;
}

int main()
{
    int m;
    cin>>n>>m;

//设置二分边界
    l=0,r=n;

//实数二分
    while (l + eps < r)
    {
        double mid = (l + r) / 2;
        if (pd(mid,m))
            r = mid;
        else
            l = mid;
    }
    cout<<fixed<<setprecision(7)<<l;

    //一般使用print
    //printf("%x.yf",n)
    //其中X是固定整数长度,小数点前的整数位数不够,会在前面补0
    //y是保留小数位数,不够补零
    //printf("%.7f",l);
    return 0;
}

总结

二分的题目主要是必须要求是单调的,一般会有条件等字眼。做这种题目主要还是找到递增或者递减的序列,然后关于序列的判定条件。或者通过观察时间复杂度来看是否可以使用二分,二分法的题目相对来说比较明显,设计起来也比较简单,模板不用死记硬背,理解一下,很快就可以独立写出来。

贪心算法

贪心算法(Greedy algorithm),又称贪婪算法。是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而使得问题得到全局最优解。

贪心的算法的设计就是要遵循某种规则,不断地选取当前最优解的算法设计方法。这节实验将会通过多个问题的来讲解贪心算法。

知识点

  • 贪心算法的基本概念
  • 贪心算法的适用范围
  • 贪心算法的设计步骤
  • 贪心算法的题目讲解

算法概念:

贪心算法与枚举法的不同之处在于每个子问题都选择最优的情况,然后向下继续进行,且不能回溯,枚举法是将所有情况都考虑然后选出最优的情况。

贪心算法,在对问题求解时,不从整体考虑,而是采用一叶障目的选择方式,只选择某种意义上的局部最优解。并且,贪心算法是没有固定的模板可以遵循的,每个题目都有不同的贪心策略,所以算法设计的关键就是贪心策略的选择。

贪心算法有一个必须要注意的事情。贪心算法对于问题的要求是,所有的选择必须是无后效性的,即当前的选择,不能影响后续选择对于结果的影响。

贪心算法主要适用于最优化问题,如:MST 问题。有时候贪心算法并不能得到最优答案,但是能得到精确答案的近似答案。有时可以辅助其他算法得到不是那么精确的结果。

符合贪心策略:

所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。

所谓的贪心选择性质就是,该问题的每一步选择都在选择最优的情况下能够导致最终问题的答案也是最优。

或者说是无后效性,如果该问题的每一步选择都对后续的选择没有影响,就可以是应用贪心算法。

贪心算法的设计步骤
按照定义设计:

  • 证明原问题的最优解之一可以由贪心选择得到。

  • 将最优化问题转化为这样一个问题,即先做出选择,再解决剩下的一个子问题。

  • 对每一子问题一一求解,得到子问题的局部最优解

  • 把子问题的解局部最优解合成原来解问题的一个解

伪代码:
对于问题Q:

while(Q.hasNextStep)
{
    Select(Q.nowBestSelect);
    Q.NextStep
}

Select(Q.nowBestSelect);

先看一个启发式问题:找零问题

题意:

假设商店老板需要找零 n 元钱。
钱币的面额有:100 元、50 元、20 元、5 元、1 元、如何找零使得所需钱币的数量最少?
注意:n 可能为 0,也能为几百元(别问,问就是来着里微信提现来了)

输入解法:
在第一行给出测试例个数 N。代表需要找零的钱数。

输入样例:
365

输出解法
  有 5 行输出数据,每一行输出数据输出找零的金额与数量,详情看样例。

输出样例:
100:3
50:1
20:0
5:3
1:0

运行限制:
最大运行时间:1s
最大运行内存:128M

关于这个题,如果是正常人都知道从大的钱开始找钱。这就是一种贪心的思想,将大问题转化为一个个小的子问题,每次选择最大的钱数使得总量最小。

其实再生活中贪心思想的例子还有很多,像是“自助餐“这种的都是贪心算法的印证。贪心算法其实离我们很近,掌握不会很困难的。

代码:

#include 
#include 
#include
using namespace std;

//面值
int t[5]={100, 50, 20, 5, 1};

//张数
int m[5];

void change(int n)
{

    for(int i=0;i<5;i++)
    {
        m[i]=n/t[i];

        n=n%t[i];

        //print("%d",n);
    }
}

int main()
{
    int N;

    cin>>N;

    change(N);

    for(int i=0;i<5;i++)
    {
        printf("%d:%d\n",t[i],m[i]);
    }
}

活动选择型问题之小 B 的宿舍

题意:

小 B 的宿舍楼沿着走廊南北向的两边各有 200 个房间。

如图所示:

[房间1][房间3][房间5][房间7][房间9 ]...[房间399]
----------------------------------------------
                   走廊
----------------------------------------------
[房间2][房间4][房间6][房间8][房间10]...[房间400]

最近,由于转专业和专业分流的原因,宿舍将迎来新的调整,以便组成新的班级后方便管理。

但是由于走廊狭窄,走廊里只能通过两个搬运的物品(可以同向也可以反向),因此必须指定高效的搬运计划。

老师给了每位同学下达了以下要求,让同学们提前收拾好行李,然后给每位同学 10 分钟的时间搬运。

当房间 i 搬运行李到 j 时,i 与 j 之间的走廊都会被占用,但是可以容纳两个不同同学同时搬运。所以,10 分钟之内同一段走廊最多两个人同时搬运,不重叠的走廊也可以同时搬运。(就是说,可以好几人各搬各的,在走廊的不同路段)

小 B 的老师是个数学老师,经过运筹学一通计算他得到了最优的搬运计划。
虽然计划不唯一,但是最优值唯一,请问这个最短时间是多少?

输入解法:
输入数据有 T 组测试例,在第一行给出测试例个数 T。

每个测试例的第一行是一个整数 N(1≤N≤200),表示要搬运行李的人数。接下来 N 行,每行两个正整数 s 和 t,表示一个人,将行李是从房间号码 s 移到到房间号码 t。

输入样例:
3
4
10 20
30 40
50 60
70 80
2
1 3
2 200
3
10 100
20 80
30 50

输出解法
  每组输入都有一行输出数据,为一整数 T,表示完成任务所花费的最小时间。

输出样例:
10
10
20

该题属于贪心算法,因为它尽可能使搬运行李同时进行,以便使单独安排的搬运次数最少。这样用的时间最少,即所用最少时间为不能同时搬运行李的次数,进而转化为寻找某一段走廊使用次数最多(贪心标准),由于走廊可以同行 2 人,所以除 2,有余数再加 1 即可,即使最多的搬运次数,再乘以 10,即为最少搬运时间。

首先将二维问题转化成一维问题。

不难发现,相对应的两个房间其实是占用一段走廊的,我们可以将将房间号映射为走廊号,然后再考虑上面的解析。

#include 
#include 
#include
using namespace std;

int main()
{

    int move[200];

//搬运次数
    int N;

//每次搬运的起点和终点

    int from, to;

    int maxAns=0;

    int T;
    cin>>T;
    while(T--)
    {
        scanf("%d", &N);
        memset(move, 0, sizeof(move));
        for(int i = 0; i < N; i++)
        {
            scanf("%d%d", &from, &to);
//将房间号映射为走廊号
            from = (from - 1)/2;
            to = (to - 1)/2;
//确保from
            if(from > to)
            {
                int temp = from;
                from = to;
                to = temp;
            }

//统计占用走廊情况,并统计最大值

            for(int j = from; j <= to; j++)
            {
                move[j]++;
                maxAns=max(maxAns,move[j]);
            }



        }
        if(maxAns%2==1) maxAns=maxAns/2+1;
        else maxAns>>1; //等价于/2

        cout<<maxAns*10<<endl;
    }
}

可拆分背包问题之贪心的自助餐

题意:

小 B 同学呢,想去吃自助餐,但是他是那种比较节俭的的人,既不想浪费食物,又想尽可能吃的贵一点,他于是私下里做了调查。

小蓝餐厅的自助餐有 n 种食材,每种食材都有它的价格。

而且也能估计出每一份的重量,所以他列了一个表格。

红烧牛肉  30300g
油闷大虾  85g
四喜丸子  48g
三文鱼    53g
排骨      18200g
麻辣兔头  20120g
高汤海参  4070g
扇贝粉丝  832g
牛排      79240g
...

现在小 B 想知道在他到底最多吃多少钱的菜品。
假设自助餐厅的菜品供应同样的菜品每个人只能取一份。
小B的饭量假设为 C,单位为 g。
现在请你设计一个程序帮助小 B 计算他的最多吃了多少钱。

输入解法
第一行输入 n C(0<=n<=1000)(0<=C<=10000)
其中 n 为菜品数量,C 为小 B 的肚子容量。

第二行输入两个数 V,W 
    第一个数 V[i] 是第 i 个菜品的价值(0<=v[i]<=10000)

    第二个数 V[i] 是第 i 个菜品的质量(0<=w[i]<=10000)

输入样例:
20 1000
1 22
2 43
123 214
12 2
123 432
21 223
22 16
77 49
34 78
34 9
43 677
21 34
23 23
12 56
332 56
21 99
123 545
389 33
12 999
23 88

输出解法
  输出一行数据,表示最大的价值,保留三位小数。

输出样例:
1204.114

可拆分背包的一般解法为:

这里有 n 种不同值 v[i] 和权重 w[i] 的对象(如果选择该对象的 w[i] 可以获得值 v[i])。

你有一个容器来挑选它们。你可以根据自己的需要把它们分成任意大小的碎片。可以拾取的对象的最大重量给定为 w。请计算您能得到的最大值。

就像是这个题目,要想吃回本就要捡着贵的吃,但是贵只是一方面,人会饱,所以用价格除以质量所获的价格商才是贪心准则,应按照价格商优先进行选取。

于是这个题,就要用的我们之前学的知识了。这里因为要整体排序,所以要先创建一个类,然后自定义 cmp 函数,在使用 sort 排序。

#include 
#include 
#include
using namespace std;

//需要一个结构体,通过性价比,能够查找到重量和价值。

//做一个排序,需要将性价比由高到底排序,排序的过程中重量和(价值)要对应上

struct Food
{
    double w;
    double v;
    double aver;

};
//C++一般用 struct,因为默认都是public的

bool cmp(Food a, Food b)
{
    return a.aver > b.aver;

    //助记大于号就是从大到小排序,小于号就是从小到大排序
}


int main()
{
    Food foods[1009];
    int n;
    double C;
    double Value = 0;
    cin >> n >> C;
    for (int i = 0; i < n; i++)
    {
        cin >> foods[i].v>>foods[i].w;

        //求性价比
        foods[i].aver = foods[i].v / foods[i].w;
        //cout << foods[i].aver << endl;

    }


    //性价比排序
    sort(foods, foods + n, cmp);

    //当背包(肚子)能装下所有物品(菜)时,直接输出所有的物品(菜品)价值之和
    //
    int sum = 0;
    for (int i = 0; i < n; i++)
    {
        sum += foods[i].w;
    }
    if (sum <= C)
    {
        for (int j = 0; j < n; j++)
            Value += foods[j].v;
        //V = floor(V * 1000.0) / 1000.0;
        cout << setiosflags(ios::fixed) << setprecision(3) <<Value << endl;
        return 0;
    }

    //当背包(肚子)不能装下所有物品时应该由性价比的顺序,选择装入的物品

    for (int i = 0; i < n; i++)
    {
        if (foods[i].w <= C)
        {
            Value =Value + foods[i].v;
            C = C - foods[i].w;
        }
        else
        {
            //直接将剩余的C加入即可
            Value =Value + C * foods[i].aver;
            C = 0;
        }
        if (C == 0)
            break;
    }
    //V = floor(V * 1000.0) / 1000.0;
    cout << setiosflags(ios::fixed) << setprecision(3) <<Value << endl;
    return 0;
}

实验总结

贪心算法的最主要的特征就是无后效性,就像是自助餐那个题目,如果说吃了某一样食物,就不能吃另一个食物了,那么这就有了后效性,那就不能使用贪心算法进行解决问题了。

本节课举了三个贪心算法的例子进行讲解,贪心算法是算法竞赛中最入门的算法。没接触过感觉很深奥,接触过了也就那样,简单的贪心伸伸手就可以写出来,其实非常简单,大家也不要过分的担心。

你可能感兴趣的:(蓝桥杯+力扣,c++)