C++笔试题模版汇总(五)动态规划/贪心

目录

揹包问题

完全揹包

线性DP

数字三角形

区间DP

石子合并

计数类DP

整数划分

数位统计DP

计数问题

状态压缩DP

蒙德里安的梦想

记忆话搜索

滑雪

线性DP

最长上升子序列

最长公共子序列

最短编辑距离

编辑距离

贪心

区间问题

区间选点

最大不相交区间数量

区间分组

区间覆盖

哈夫曼树

贪心排序不等式

排队打水

贪心绝对值不等式

货仓选址

贪心推公式

耍杂技的牛

阿里笔试题

1、养鸡场问题

2、求序列期望

快手笔试真题

判断英文单词大写字母用法是否正确

数字解码字母

简化文件路径

Leetcode 546.移除盒子


揹包问题

问题:

有 N件物品和一个容量是 V的揹包。每件物品只能使用一次。第 i件物品的体积是 vi,价值是 wi。求解将哪些物品装入揹包,可使这些物品的总体积不超过揹包容量,且总价值最大。输出最大价值。

分析:每次只能选择一个物品。有4个物品,最大容量是5:2和3加起来刚好不超过揹包最大体积,所以最大为8.

动态规划要用两个方面来表示:

1、状态表示:需要几维,f(i, j);包括集合是什么?属性(所有选法的最max/min 数量)?揹包问题属于集合。

集合需要考虑的条件:所有选法中 1)只考虑前i个物品 2)总体积不超过j;得到总价值最大的集合

2、状态计算

表示集合划分,将这个集合划分多个小的集合。

原则:不重复/不漏

比如 从1~i 中选总体积不超过j的最大价值。可以理解为从1~i-1中选出总体积不超过j的最大价值。==》f(i - 1, j);

/*
f[i][j]:
1.不选第一个物品:f[i][j] = f[i - 1][j];
2.选第i个物品:f[i][j] = f[i - 1][j - v[i]]
f[i][j] = max(1, 2)
f[0][0] = 0;
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;//n表示物品个数,m表示揹包容量
 
int v[N], w[N];//体积,价值
//暴力做法
int f[N][N];//存所有状态
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i ++)
        for (int j = 0; j <= m; j ++)
        {
            f[i][j] = f[i - 1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
    cout << f[n][m] << endl;
    return 0;
}
 
//优化做法:使用滚动数组来做,
//如果f(i)只用到了f(i - 1),缩到一维来做,交替来算。f(0)和f(1)交替来算
// int f[N];//所有状态
// int main() 
// {
//     cin >> n >> m;
//     for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
//     for (int i = 1; i <= n; i ++)
//         for (int j = m; j >= v[i]; j --)//枚举体积
//             f[j] = max(f[j], f[j - v[i]] + w[i]);//找最大的
//     cout << f[m] << endl;
//     return 0;
// }

完全揹包

完全揹包跟揹包问题只有一个区别:每种物品都有无限件可用。有 N种物品和一个容量是V 的揹包,每种物品都有无限件可用。第 i种物品的体积是 vi,价值是 wi。求解将哪些物品装入揹包,可使这些物品的总体积不超过揹包容量,且总价值最大。输出最大价值。

跟揹包问题状态计算不一样了,f(i, j)分为选和不选两种问题,但是这一次可以无限用。需要划分无数个子集。先考虑朴素怎么做,然后在找优化方法。

从1~i-1开始选

总结:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;//n表示物品个数,m表示揹包容量
int v[N], w[N];//体积,价值
//朴素做法
// int f[N][N];//存所有状态
// int main()
// {
//     cin >> n >> m;
//     for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
//     for (int i = 1; i <= n; i ++)
//         for (int j = 0; j <= m; j ++)
//         {
//             f[i][j] = f[i - 1][j];
//             if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
//         }
//     cout << f[n][m] << endl;
//     return 0;
// }
 
//优化做法
int f[N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i ++)
        for (int j = v[i]; j <= m; j ++)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m] << endl;
    return 0;
}

线性DP

数字三角形

问题:给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

思路:每一次只能走一个格子,有很多条路可以走。找到一条路径上所有数字之和最大的。

首先先把他分成i 行j列,用f[i][j]表示所有起点到f[i][j]点所有路径之和的集合。比如第四行第二个点为7表示为(4, 2);从最后的终点往前递推:如图划圈的点7,所以从起点到f[i][j]点路径分成两类:一种是从左上方一种是来自右上方。

左上方:比如从7那个点到8,所以需要往上走一格即f[i - 1][j - 1] 并且得加上该点的值a[i][j];

右上方:比如画圈那个点右上方的1,跟左上方的点8在同一行,但是不同列,所以走一格即f[i - 1, j] + a[i][j];

然后不断往上递归,直到到达起点为止。最后将两种情况取max。如下图所示:
 

注意边界问题:如果涉及到i-1的下标循环得从i = 1开始。动态规划时间复杂度如何求:状态数量 * 转移的计算量

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int a[N][N];//表示每个点的值
int f[N][N];//存从起点到第i,j点的路径最大长度
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= i; j ++)//这里有问题是<=i,不是<=n
            scanf("%d", &a[i][j]);
    //初始化,这里必须注意,从0开始到n,然后每一列得多+1,因为三角形最右边有边界,求f[i][j]的时候会遍历到每列最右边的点,然后他的右上角的点实际上不存在的,所以初始化的时候必须把它初始化成INF
    for (int i = 0; i <= n; i ++)
        for (int j = 0; j <= i + 1; j ++)
            f[i][j] = -INF;//这里得是负无穷
    f[1][1] = a[1][1];//第一个点就是他本身的值
    //i从2开始
    for (int i = 2; i <= n; i ++)
        for (int j = 1; j <= i; j ++)
            f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);//求左上角右上角那个点的最大值遍历
    //这里又一个问题,如果是最后一行,则必须得把最后一行的最大值求出来
    //最终答案是要遍历最后一行,最后一行可能会走到每一个位置,求终点的最大值,然后枚举起点到终点的最大值
    int res = -INF;//这里得是负无穷
    for (int i = 1; i <= n; i ++) res = max(res, f[n][i]);
    printf("%d\n", res);
    return 0;
}

区间DP

石子合并

设有N堆石子排成一排,其编号为1,2,3,…,N。每堆石子有一定的质量,可以用一个整数来描述,现在要将这N堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有4堆石子分别为 1 3 5 2, 我们可以先合并1、2堆,代价为4,得到4 5 2, 又合并 1,2堆,代价为9,得到9 2 ,再合并得到11,总代价为4+9+11=24;如果第二步是先合并2,3堆,则代价为7,得到4 7,最后一次合并代价为11,总代价为4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

思路:假设有一堆石子,1,3,5,2,我们把1,3合并,5,2合并,总共是4 + 7 + 11 = 22.是最小代价。如下图所示:
 

所有合并的个数,如果选取堆数有n - 1次选择,然后第二次从n- 1中选就有n -2次选择--》(n - 1)*(n - 2)*.....

状态表示f[i][j],集合:所有将i到j合并成一堆的方案的集合。(j - i)!。属性:min,集合中付出的最小代价。

状态计算:化整为零的过程,把f[i][j]分解成若干个子问题,分而治之。实际上就是从最后一步开始往前递推。

最小方案:min(f(i, k)) +min(f(k + 1, j))+从i到j的部分和s[i] - s[i - 1];

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510;
int n;
int s[N];//前缀和
int f[N][N];
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++) cin >> s[i], s[i] += s[i - 1];//更新前缀和
    for (int len = 2; len <= n; len ++)//len从2开始,如果从1开始没有意义
        for (int i = 1; i + len - 1 <= n; i ++)//枚举区间左端点:i+ len - 1是左边端点
        {
            int j = i + len - 1;//枚举右端点
            //枚举之前
            f[i][j] = 1e8;//先将i,J初始化成一个特别大的值
            for (int k = i; k < j; k ++)//枚举k
                //式子直接抄过来
                f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
        }
    //把f[1][n]带入定义就是所有将1-n合并的方案最大值
    cout << f[1][n] << endl;
    return 0;
}

计数类DP

整数划分

https://blog.csdn.net/qq_27262727/article/details/105382085

一个正整数nn可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中n1≥n2≥…≥nk,k≥1。我们将这样的一种表示称为正整数n的一种划分。现在给定一个正整数n,请你求出n共有多少种不同的划分方法。比如:5,有七种表示方式。

n中的数可以使用无限次,所以可以把它看成完全揹包问题。先回顾一下完全揹包具体做法:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N];//存所有状态
int main()
{
    cin >> n;
    f[0] = 1;
    for (int i = 1; i <= n; i ++)
        for (int j = i; j <= n; j ++)//j是容量
            f[j] = (f[j] + f[j - i]) % mod;
    cout << f[n] << endl;
    return 0;
}

数位统计DP

计数问题

一个正整数nn可以表示成若干个正整数之和,形如:n=n1+n2+…+nkn=n1+n2+…+nk,其中n1≥n2≥…≥nk,k≥1n1≥n2≥…≥nk,k≥1。

我们将这样的一种表示称为正整数n的一种划分。现在给定一个正整数n,请你求出n共有多少种不同的划分方法。

思路:暴力做法,出现几个1就统计几次,时间复杂度是10^8*8

优化做法:分情况讨论,可以转化为求1~n中x出现的次数,然后求一个前缀的答案,然后两个相减

以x = 1为例,看看怎么求:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N];//存所有状态
int main()
{
    cin >> n;
    f[0] = 1;
    for (int i = 1; i <= n; i ++)
        for(int j = i; j <= n; j ++)//j是容量
            f[j] = (f[j] + f[j - i]) % mod;
    cout << f[n] << endl;
    return 0;
}

状态压缩DP

蒙德里安的梦想

求把N*M的棋盘分割成若干个1*2的的长方形,有多少种方案。例如当N=2,M=4时,共有5种方案。当N=2,M=3时,共有3种方案。

思路:

1、所谓的状态压缩DP,就是用二进制数保存状态。为什么不直接用数组记录呢?因为用一个二进制数记录方便作位运算。前面做过的八皇后,八数码,也用到了状态压缩。

 2. 本题等价于找到所有横放 1 X 2 小方格的方案数,因为所有横放确定了,那么竖放方案是唯一的。

 3. 用f[i][j]记录第i列第j个状态。j状态位等于1表示上一列有横放格子,本列有格子捅出来。转移方程很简单,本列的每一个状态都由上列所有“合法”状态转移过来f[i][j] += f[i - 1][k]

 4. 两个转移条件: i 列和 i - 1列同一行不同时捅出来 ; 本列捅出来的状态j和上列捅出来的状态k求或,得到上列是否为奇数空行状态,奇数空行不转移。

 5. 初始化条件f[0][0] = 1,第0列只能是状态0,无任何格子捅出来。返回f[m][0]。第m + 1列不能有东西捅出来。
 

#include<bits/stdc++.h>
using namespace std;
const int N = 12, M = 1 << N;
int st[M];
long long f[N][M];
 
 
int main(){
    int n, m;
    while (cin >> n >> m && (n || m)){
 
        for (int i = 0; i < 1 << n; i ++){
            int cnt = 0;
            st[i] = true;
            for (int j = 0; j < n; j ++)
                if (i >> j & 1){
                    if (cnt & 1) st[i] = false; // cnt 为当前已经存在多少个连续的0
                    cnt = 0;
                }
                else cnt ++;
            if (cnt & 1) st[i] = false; // 扫完后要判断一下最后一段有多少个连续的0
        }
 
        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++)
            for (int j = 0; j < 1 << n; j ++)
                for (int k = 0; k < 1 << n; k ++)
                    if ((j & k) == 0 && (st[j | k])) 
                    // j & k == 0 表示 i 列和 i - 1列同一行不同时捅出来
                    // st[j | k] == 1 表示 在 i 列状态 j, i - 1 列状态 k 的情况下是合法的.
                        f[i][j] += f[i - 1][k];      
        cout << f[m][0] << endl;
    }
    return 0;
}

记忆话搜索

滑雪

给定一个R行C列的矩阵,表示一个矩形网格滑雪场。矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。

一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。
思路:从中间开始滑,从大到小最多能滑25个格子。

状态表示:

集合:f[i][j]表示状态,从f[i][j]开始滑,所有表示从i,j开始滑的所有路径。属性:max

状态计算:就是搜索,每个点能不能向上下左右动

下面代码中的f[x][y] = max(f[x][y],dp(xx,yy)+1);
实际上就是向四个方向判断之后转移:
if(a[i-1][j]<now) f[i][j] = max(f[i][j],f[i-1][j]+1);//上
if(a[i+1][j]<now) f[i][j] = max(f[i][j],f[i+1][j]+1);//下
if(a[i][j-1]<now) f[i][j] = max(f[i][j],f[i][j-1]+1);//左
if(a[i][j+1]<now) f[i][j] = max(f[i][j],f[i][j+1]+1);//右
 

#include<bits/stdc++.h>
#define read(x) scanf("%d",&x)
using namespace std;
const int N = 310;
int n,m,a[N][N],f[N][N];
int dx[4] = {-1,0,1,0};
int dy[4] = {0,1,0,-1};
int dp(int x,int y) {
    if(f[x][y]!=0) return f[x][y]; //记忆化的好处 
 
    f[x][y] = 1;//初始化 
    for(register int i=0; i<4; i++) {
        int xx = x+dx[i];
        int yy = y+dy[i];
        if(xx>=1&&xx<=n && yy>=1&&y<=m && a[x][y]>a[xx][yy])
            f[x][y] = max(f[x][y],dp(xx,yy)+1);
    }
    return f[x][y];
}
 
int main() {
    read(n),read(m);
    for(register int i=1; i<=n; i++)
        for(register int j=1; j<=m; j++)
            read(a[i][j]);
    int ans = 0;
    for(register int i=1; i<=n; i++)
        for(register int j=1; j<=m; j++)
            ans = max(ans,dp(i,j));
    printf("%d\n",ans);
    return 0;
}

线性DP

最长上升子序列

给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。

思路:如下图所示数值严格单调递增的子序列最长长度是4.

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n;
int a[N], f[N];
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++) cin >> a[i];
    for (int i = 1; i <= n; i ++)
    {
        f[i] = 1;//设f[i]默认为1,找不到前面数字小于自己的时候就为1
        for (int j = 1; j < i; j ++)
            if (a[j] < a[i])
                f[i] = max(f[i], f[j] + 1);// 前一个小于自己的数结尾的最大上升子序列加上自己,即+1
    }
    int res = 0;
    for (int i = 1; i <= n; i ++) res = max(res, f[i]);
    cout << res << endl;
}

最长公共子序列

给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。

#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
    cin >> n >> m >> a + 1 >> b + 1;
    for (int i = 1; i <= n; i ++)////要从1开始读取,因为会用到i-1
        for (int j = 1; j <= m; j ++)
        {
            f[i][j] = max(f[i - 1][j], f[i][j - 1]);
            if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
        }
    cout << f[n][m] << endl;
    return 0;
}

最短编辑距离

给定两个字符串A和B,现在要将A经过若干操作变为B,可进行的操作有:

  1. 删除–将字符串A中的某个字符删除。
  2. 插入–在字符串A的某个位置插入某个字符。
  3. 替换–将字符串A中的某个字符替换为另一个字符。

现在请你求出,将A变为B至少需要进行多少次操作。

1)删除操作:把a[i]删掉之后a[1~i]和b[1~j]匹配
            所以之前要先做到a[1~(i-1)]和b[1~j]匹配
            f[i-1][j] + 1
2)插入操作:插入之后a[i]与b[j]完全匹配,所以插入的就是b[j] 
            那填之前a[1~i]和b[1~(j-1)]匹配
            f[i][j-1] + 1 
3)替换操作:把a[i]改成b[j]之后想要a[1~i]与b[1~j]匹配 
            那么修改这一位之前,a[1~(i-1)]应该与b[1~(j-1)]匹配
            f[i-1][j-1] + 1
            但是如果本来a[i]与b[j]这一位上就相等,那么不用改,即
            f[i-1][j-1] + 0
最后f[i][j]就由以上三个可能状态转移过来,取个min。
 

/*
1)删除操作:把a[i]删掉之后a[1~i]和b[1~j]匹配
            所以之前要先做到a[1~(i-1)]和b[1~j]匹配
            f[i-1][j] + 1
2)插入操作:插入之后a[i]与b[j]完全匹配,所以插入的就是b[j] 
            那填之前a[1~i]和b[1~(j-1)]匹配
            f[i][j-1] + 1 
3)替换操作:把a[i]改成b[j]之后想要a[1~i]与b[1~j]匹配 
            那么修改这一位之前,a[1~(i-1)]应该与b[1~(j-1)]匹配
            f[i-1][j-1] + 1
            但是如果本来a[i]与b[j]这一位上就相等,那么不用改,即
            f[i-1][j-1] + 0
最后f[i][j]就由以上三个可能状态转移过来,取个min
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
    cin >> n >> a + 1 >> m >> b + 1;
    //如果a/b序列为空则需要增加操作
    for (int i = 0; i <= m; i ++) f[0][i] = i;
    for (int i = 0; i <= n; i ++) f[i][0] = i;
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= m; j ++)
        {
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);//这里搞错了是a[i] == b[j],j别看错了
            else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
        }
    cout << f[n][m] << endl;
    return 0;
}

编辑距离

问题:给定n个长度不超过10的字符串以及m次询问,每次询问给出一个字符串和一个操作次数上限。对于每次询问,请你求出给定的n个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。每个对字符串进行的单个字符的插入、删除或替换算作一次操作。

思路:这题就是上一个求几遍最短编辑距离就行了。

时间复杂度:1e6 * 10^2 = 1e8,时限两秒,是ok的
 

#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 15, M = 1001;//N = 15字符串最大长度,M是最大询问
int n, m, f[N][N];
char s[M][N];
int edit_dis(char a[], char b[])
{
    int lena = strlen(a + 1);
    int lenb = strlen(b + 1);
    for (int i = 1; i <= lena; i ++) f[i][0] = i;
    for (int i = 1; i <= lenb; i ++) f[0][i] = i;
    for (int i = 1; i <= lena; i ++)
        for (int j = 1; j <= lenb; j ++)
        {
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            //这里写错了是a[i] == b[j]
            if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
            else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
        }
    return f[lena][lenb];
}
int main()
{
    cin >> n >> m;
    //输入是i < n,不需要<=,因为i是从0开始的
    for (int i = 0; i < n; i ++) cin >> s[i] + 1;
    while (m --)
    {
        char q[N];
        int limit;
        cin >> q + 1 >> limit;
        int ans = 0;
        for (int i = 0; i < n; i ++)
            if (edit_dis(s[i], q) <= limit) ans ++;
        cout << ans << endl;
    }
    return 0;
}

贪心

区间问题

区间选点

给定N个闭区间[ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。输出选择的点的最小数量。

位于区间端点上的点也算作区间内。

思路:

当一个数上有点时,包含这个数的区间都会被满足。因此,我们在推理时,应尽可能“一箭多雕”。

接着,我们的目标就转化为“如何尽可能完美地放点”。一个区间,若放较前,则无法顾及后面;若放较后,则无法顾及前面。既然如此,我们就应该有规律地放(从前往后或从后往前,此处讲从前往后)。故先要储存,排序。

要排序,就得有关键字。关键字分为二:1.起点、2.终点。若以起点为关键字,我们就不知道点该尽量往哪放。既然从前往后,理应尽量往后放,因为其他区间都在自己后面。可万一有一个区间起点在自己之后,终点在自己之前,那么它就会被巧妙地避开,最后WA。

所以,我们要以终点为关键字。这样,我们只要将点放在终点的数上就能将尽可能多的区间满足。步骤如下:
1、输入;
2、储存;
3、排序;
4、处理;
5、输出。
 

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
//结构体排序
struct Range
{
    int l, r;
    bool operator< (const Range &w)const
    {
        return r < w.r;
    }
}range[N];
int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++)
    {
        int l, r;
        cin >> l >> r;
        range[i] = {l, r};
    }
    sort(range, range + n);
    int res = 0, ed = -2e9;
    //枚举每个区间
    for (int i = 0; i < n; i ++)
        if (range[i].l > ed)
        {
            res ++;
            ed = range[i].r;
        }
    cout << res << endl;
    return 0;
}

最大不相交区间数量

给定N个闭区间[ai,biai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。输出可选取区间的最大数量。

思路:做法跟区间选点操作是一样的。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
//结构体排序
struct Range
{
    int l, r;
    bool operator< (const Range &w)const
    {
        return r < w.r;
    }
}range[N];
int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++)
    {
        int l, r;
        cin >> l >> r;
        range[i] = {l, r};
    }
    sort(range, range + n);
    int res = 0, ed = -2e9;
    //枚举每个区间
    for (int i = 0; i < n; i ++)
        if (range[i].l > ed)
        {
            res ++;
            ed = range[i].r;
        }
    cout << res << endl;
    return 0;
}

区间分组

给定N个闭区间[ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。输出最小组数。

思路:

这个区间贪心问题,是要按照区间左端点排序。

分情况讨论贪心决策:

1.如果一个区间的左端点比当前每一个组的最右端点都要小,那么意味着要开一个新区间了,这个条件还可以优化成,一个区间左端点比最小组的右端点都要小就开一个新组。
2.如果一个区间的左端点比最小组的右端点大,那么就放在该组,这其实也是一个贪心,因为是先考虑最容易放入一个区间的组

这道题对于数据结构上的选择也要考虑,用一个小顶堆,也就是优先队列来存储每一个组的最右端点是最好的数据结构了。

步骤
1.按区间左端点排序
2.扫描所有区间,按以上情况分开处理
3.最后堆中存的所有数据的个数就是组的个数

#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n;
struct Range
{
    int l, r;
    //< 排序
    bool operator< (const Range &w)const
    {
        return l < w.l;
    }
}range[N];
int main()
{
    //输入
    cin >> n;
    for (int i = 0; i < n; i ++)
    {
        int l, r;
        cin >> l >> r;
        range[i] = {l, r};
    }
    //排序
    sort(range, range + n);
    //定义小根堆,定义语法如下
    //来维护所有组的最大值
    priority_queue<int, vector<int>, greater<int>> heap;
    for (int i = 0; i < n; i ++)
    {
        //用r来代表区间
        auto r = range[i];
        //如果堆为空或者堆顶最小值>=i区间左端点,区间需要开一个新的组
        if (heap.empty() || heap.top() >= r.l) heap.push(r.r);
        else
        {//否则这个区间放在最小值组中
            int t = heap.top();
            heap.pop();//出堆,把最小值堆顶删掉删掉
            heap.push(r.r);//加入新的右端点放进去
        }
    }
    cout << heap.size() << endl;//最后输出组的数量
    return 0;
}

区间覆盖

给定N个闭区间[ai,bi]以及一个线段区间[s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。输出最少区间数,如果无法完全覆盖则输出-1。

思路:

分析:令需要覆盖的区间开头为st,结尾为ed

1.将所有的区间按左端点排序
2.找到能覆盖st的区间中右端点最大的那一个,从前往后枚举每个区间,在所有能覆盖start的区间中,选择右端点最大的区间,然后将start更新成右端点的最大值
3.更新st,最后判断ed是否被覆盖就可以了

证明
在剩下所有能覆盖start的区间中,选择右端点最大的区间,则一定会比前面的选择最优,更快达到end,所以该做法一定是最优。
时间复杂度 O(nlogn)
 

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range
{
    int l, r;
    bool operator< (const Range &w)const
    {
        return l < w.l;
    }
}range[N];
int main()
{
    int st, ed;
    cin >> st >> ed >> n;
    //cin >> n;
    for (int i = 0; i < n; i ++)
    {
        int l, r;
        cin >> l >> r;
        range[i] = {l, r};
    }
    sort(range, range + n);
    int res = 0;
    bool success = false;
    for (int i = 0; i < n; i ++)
    {
        int j = i, r = -2e9;
        while (j < n && range[j].l <= st)//找到一个能覆盖st并且右端点最长的值
        {
            r = max(r, range[j].r);
            j ++;
        }
        if (r < st)//如果最后找到的值没有能覆盖st的就break,如果没有这一步,遇到全部都是大于st的区间就会TLE
        {
            res = -1;
            break;
        }
        res ++;
        if (r >= ed)//如果st已经大于ed了就break
        {
            success = true;
            break;
        }
        st = r;//更新st
        i = j - 1;
    }
    if (!success) res = -1;//判断一下最后覆盖到的区间是否已经过了ed
    cout << res << endl;
    return 0;
}

哈夫曼树

在一个果园里,达达已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。

达达决定把所有的果子合成一堆。

每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。

可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。

达达在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以达达在合并果子时要尽可能地节省体力。

假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使达达耗费的体力最少,并输出这个最小的体力耗费值。

例如有3种果子,数目依次为1,2,9。

可以先将1、2堆合并,新堆数目为3,耗费体力为3。

接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。

所以达达总共耗费体力=3+12=15。

可以证明15为最小的体力耗费值。

哈夫曼树:

树是完全二叉树,所有叶子结点都是合并的点。

过程: 总和:a根据路径长度会算三次,同理其他一样。

方法:每次挑出值最小的来合并。

证明:

1、如果f比b小,但是f比b浅,可以进行交换一下,把最小的点放到最深的地方。意味着第一步就可以合并。

2、n->n-1,n-1的最优解就是n的最优解。当两个最小值的点合并则变成如下图所示,把剩下n-1的最小值用f(n-1)这个方案来表示,总代价就是 f(n) = f(n - 1) + a + b;,第一次合并需要a+b的代价。剩下的问题就变成n-1的问题,从n-1的点挑选最小的两个值合并,往往复复。
 

//每次求最小值用堆优先队列来做
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
int main()
{
    int n;
    cin >> n;
    //定义小根堆
    priority_queue<int, vector<int>, greater<int>> heap;
    while (n --)
    {
        int x;
        cin >> x;
        //入堆
        heap.push(x);
    }
    int res = 0;//存结果即最小的体力耗费值
    while (heap.size() > 1)//只要堆当中元素个数大于1
    {
        //取出两个最小的值,进行合并
        int a = heap.top();heap.pop();
        int b = heap.top();heap.pop();
        res += a + b;
        heap.push(a + b);//把合并结果入堆
    }
    cout << res << endl;
    return 0;
}

贪心排序不等式

排队打水

有 n个人排队到 1 个水龙头处打水,第 i个人装满水桶所需的时间是 ti,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?

思路:

按照从小到大排序,总时间最小

证明:反证法

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int t[N];
int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++) cin >> t[i];
    sort(t, t + n);
    LL res = 0;
    //这里注意sort是从小到大排序,最小的应该*最大的值n - 1,所以得倒过来
    for (int i = 0; i < n; i ++) res += t[i] * (n - i - 1);
    cout << res << endl;
    return 0;
}

贪心绝对值不等式

货仓选址

在一条数轴上有 NN 家商店,它们的座标分别为 A1A1~ANAN。

现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。

为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

思路:

货仓选址.png

时间复杂度(O(nlog(n))

#include <iostream>
#include <algorithm>
#include <cstring>
 
using namespace std ;
 
typedef long long LL ;
const int N = 100010 ;
 
int q[N] ;
int n ;
 
int main(){
    cin >> n ;
 
    for(int i=1;i<=n;i++){
        cin >> q[i] ;
    }
 
    sort(q+1,q+1+n) ;
    LL res = 0 ;
 
    for(int i=1;i<=n;i++){
        res += abs(q[i]-q[(n+1)/2]) ;
    }
 
    cout << res << endl ;
 
    return 0 ;
}

贪心推公式

耍杂技的牛

农民约翰的N头奶牛(编号为1..N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。

奶牛们不是非常有创意,只提出了一个杂技表演:

叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。

奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。

这N头奶牛中的每一头都有着自己的重量Wi以及自己的强壮程度Si。

一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。

您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。

思路:
 

把公共部分去掉

满足这个等式,危险系数就会降低:

把牛的最大值从小到大排序,然后顺便计算一下他的能力值,就能求出结果。

//为了使风险值的最大值最小,应该将牛按照W+S从小到大的顺序从下往上排列。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
 
vector<int> sums;
 
bool cmp(int a, int b)
{
    return sums[a] < sums[b];
}
 
int main()
{
    int N, W, S;
    cin >> N;
    vector<int> Ws(N), Ss(N), ranks(N);
    sums.resize(N);
    for (int i = 0; i < N; i++) {
        cin >> W >> S;
        Ws[i] = W;
        Ss[i] = S;
        sums[i] = W + S;
        ranks[i] = i;
    }
 
    sort(ranks.begin(), ranks.end(), cmp);
 
    int res = -1000000000;
    int sum_W = 0;
    for (int i = 0; i < N; i++) {
        int cur_id = ranks[i];
        res = max(res, sum_W - Ss[cur_id]);
        sum_W += Ws[cur_id];
    }
 
    cout << res;
 
    return 0;
}

阿里笔试题

1、养鸡场问题

小强有n个养鸡场,弟i个养鸡场初始有a[i]只小鸡。与其他养鸡场不同的是,他的养鸡场每天增加k只小鸡,小强每天结束都会在数量最多的养鸡场里卖掉一半的小鸡,假如一个养鸡场有x只鸡,则卖出后只剩下x/2(向下取整)只鸡。问m天后小强的n个养鸡场一共多少只小鸡?

输入 第一行输入三个int类型n,m,k(1 <= n,m,k <= 10^6) 第二行输入n个正整数,表示n个养鸡场初始鸡的个数

输出 输出一个整数表示鸡的总数

示例 输入:

3 3 100
100 200 400

输出:

925

思路:优先队列,时间复杂度O(mlogn)

//步骤:
//1、使用大根堆来存每个鸡场鸡的数量
//2、先将每个鸡常数量入堆,求鸡场鸡数量的总和
//3、m天增加鸡的数量:每天增加k只小鸡,将堆的top出队并卖掉一半即:
/*
        t = heap.top() + base;//top是数量最多的养鸡场,加上每天增加的鸡
        //然后卖掉一半的鸡
        int d = (t + 1) / 2;
        heap.pop();//把顶部出队
        heap.emplace(t - d - base);//加入卖掉鸡之后剩下的鸡
*/
//4、得到m天鸡的总数:除了总和之外得加上n个养鸡场每天增加的小鸡

#include <bits/stdc++.h>//包含了目前c++所包含的所有头文件
using namespace std;
typedef long long ll;//数据范围10^9所以需要long long
int main()
{
    int n, m, k, t;
    ll base(0), sum(0);//定义
    cin >> n >> m >> k;
    priority_queue<int> heap;//默认大根堆
    for (int i = 0; i < n; i ++)
    {
        cin >> t;
        heap.emplace(t);//入堆
        sum += t;//求鸡的总和
    }
    for (int i = 0; i < m; i ++)
    {
        base += k;//每一天增加k只小鸡
        t = heap.top() + base;//top是数量最多的养鸡场,加上每天增加的鸡
        //然后卖掉一半的鸡
        int d = (t + 1) / 2;
        heap.pop();//把顶部出队
        heap.emplace(t - d - base);//加入卖掉鸡之后剩下的鸡
        sum -= d;//总数减去卖掉的鸡的数量
    }
    cout << base*n + sum << endl;//除了总和之外得加上n个养鸡场每天增加的小鸡
    return 0;
}


2、求序列期望

小强得到了长度为n的序列,但他只对非常大的数字感兴趣,因此随机选择这个序列的一个连续子序列,并求这个序列的最大值,请告诉他这个最大值的期望是多少?

输入 第一行n表示序列长度接下来一行n个数描述这个序列,n大于等于1小于等于1000000,数字保证是正整数且不超过100000 第二行n个数字表示序列的值

输出 保留6位小数

样例 输入:

3
1 2 3
输出:

2.333333
先得理解求最大值期望是什么意思?

比如有这一组子序列:{1},{2},{3},{1,2},{2,3},{1,2,3},则有1最大的概率1/6,2最大的概率为2/6,3最大概率3/6,期望14/6

思路:单调栈 + 动态规划,时间复杂度O(n) 在序列x中,长度为1的子序列有n个,长度为2的子序列有n-1个...长度为n-1的子序列有2个,长度为n的子序列有1个,总的序列数:c = n+(n-1)+...+2+1 = n*(n+1)/2 个,每个出现的概率相同;

考虑以x[i]为结尾的子序列,这些子序列中有两种情况,一种是最大值为x[i],两一种是最大值不为x[i];最大值不为x[i]的相当于x[i]没有加入,可以借助之前的状态求解;最大值为x[i]的情况只需记录有多少个。

用单调栈的思路,从大到小存放出现的元素,并记录值对应的index值。

#include <bits/stdc++.h>//包含了目前c++所包含的所有头文件
using namespace std;
typedef long long ll;//数据范围10^9所以需要long long
typedef pair<int, int> PII;
const int N = 1000006;
double dp[N];//dp[i]表示前i个数中最大的期望
int main()
{
    int n, t;
    cin >> n;
    ll c = (ll)n * (n + 1) / 2;//总的序列数
    dp[0] = 0;
    stack<PII> m;//定义单调栈
    double res = 0;//最大值的期望总和
    for (int i = 0; i < n; i ++)
    {
        cin >> t;//选择第t个子序列
        //判断栈不为空并且讲小于t的元素出堆
        //单调栈pair (first, second), first为给定的n个数,second为first的个数
        while(!m.empty() && m.top().first <= t) m.pop();//如果栈不为空,并且栈顶第一个元素<=t,则出栈
        //计算i最大的序列个数,如果为空则就是当前序列+1,反则为i-栈顶部的个数
        int d = m.empty() ? i + 1 : i - m.top().second;//如果栈为空则d = i + 1,否则为i - m.top().second
        //求前i+1个数中最大的期望
        dp[i + 1] = 1.0 * t * d / c + dp[i + 1 - d];
        //期望累加
        res += dp[i + 1];
        m.emplace(t, i);//存t个连续子序列i个序列,emplace函数在容器中直接构造元素
    }
    cout << res << endl;
}

快手笔试真题

判断英文单词大写字母用法是否正确

思路:

其实只要记录有多少个大写字母即可,在遍历过程中,如果大写字母的个数小于正在遍历的下标,说明不符合题解,既不是连续的出现大写字母,如 “AaAa” 遍历到第二个 A 时的情况。

最终判断是否为全大写或只是首字母大写即可。

class Solution {
public:
    bool detectCapitalUse(string word) {
        int uc = 0;
        for (int i = 0; i < word.size(); i++) {
            if (isupper(word[i]) && uc++ < i) {//来判断字符c是否为大写英文字母
                return false;
            }
        }
        
        return uc == word.size() || uc <= 1;
    }
};

数字解码字母

一条报文包含字母A-Z,使用下面的字母-数字映射进行解码

  1. 'A' -> 1

  2. 'B' -> 2

  3. ...

  4. 'Z' -> 26

给一串包含数字的加密报文,求有多少种解码方式 举个例子,已知报文"12",它可以解码为AB(1 2),也可以是L (12) 所以解码方式有2种。

**解题思路:**动态规划(使用递归会超时)
这题有点像跳台阶,最后面那个字符i可以单独一个,也可以和前面的字符i-1合并到一起,这个有点像最后一个台阶跳一步还是两步。跳一步的话是在DP[i-1]的基础上跳,跳两步的话是在DP[i-2]的基础上跳,因此跳台阶那题的递推公式为DP[i]=DP[i-1]+DP[i-2]。而这题的递归公式也是类似的。

举个栗子:
当前给定的字符串是:“1226”
当前位置:0,则可能的编码为1
当前位置:1,则可能的编码为1-2,12
当前位置:2,则可能的编码为1-2-2,12-2,1-22
当前位置:3,则可能的编码为1-2-2-6,12-2-6,12-26,1-22-6,1-2-26(这里单独一位就是在位置2的基础上加上6,合并就是在位置1的基础上加上26)

解释:若当前在位置1,则当前位置的字符’2’可以选择与前面那个字符合并或者自己单独一位(这就类似于跳两步和跳一步),如果是自己单独一位,那么DP[i]=DP[i-1]。如果是要和前面一位合并,DP[i]=DP[i-2]。若两种情况都是满足的,那么DP[i]=DP[i-1]+DP[i-2]。

在进行DP的时候还需要考虑几种特殊的情况:
(1)当前位置i的字符为’0’,那么不能考虑单独一位,只能考虑合并
(2)当前位置和前面的位置合并的时候>‘26’,那么只能考虑单独一位
 

class Solution {
public:
    int numDecodings(string s) {
        if (s.size() == 0)
            return 0;
 
        vector<int> decodeNum = vector<int>(s.size() + 1, 0);
        decodeNum[0] = 1;
        decodeNum[1] = s[0] == '0' ? 0 : 1;
        for (int i = 2; i <= s.size(); ++i) {
            if (s[i - 1] != '0')
                decodeNum[i] = decodeNum[i - 1];
            int num = stoi(s.substr(i - 2, 2));
            if (num >= 10 && num <= 26)
                decodeNum[i] += decodeNum[i - 2];
        }
        return decodeNum[s.size()];
    }
};

简化文件路径

给定一个文档 (Unix-style) 的完全路径,请进行路径简化。

例如,

path = "/home/", => "/home"

path = "/a/./b/../../c/", => "/c"

思路:边界情况:

  • 你是否考虑了 路径 = "/../" 的情况?

在这种情况下,你需返回 "/" 。

  • 此外,路径中也可能包含多个斜杠 '/' ,如 "/home//foo/" 。

在这种情况下,你可忽略多余的斜杠,返回 "/home/foo" 

这道题的要求是简化一个Unix风格下的文件的绝对路径。

字符串处理,".."是返回上级目录(如果是根目录则不处理),重复连续出现的'/',只按1个处理, 如果路径名是".",则不处理;

class Solution {
public:
    string simplifyPath(string path)
    {
        int len = path.size();
        string str = "";
        stack<string> q;
        for(int i = 0; i < len; i++)
        {
            if(i == len - 1 && path[i] != '/')
                str += path[i];
            if(path[i] == '/' && str == "")
                continue;
            else if(path[i] ==  '/' || i == len - 1)
            {
                if(str == "..")
                {
                    if(!q.empty())
                        q.pop();
                }
                else if(str == ".")
                {

                }
                else
                {
                    q.push(str);
                }
                str = "";
            }
            else
                str += path[i];
        }
        string res = "";
        while(!q.empty())
        {
            res = q.top() + res;
            res = "/" + res;
            q.pop();
        }
        if(res == "")
            return "/";
        return res;
    }
};

Leetcode 546.移除盒子

给出一些不同颜色的盒子,盒子的颜色由数字表示,即不同的数字表示不同的颜色。你将经过若干轮操作去去掉盒子,直到所有的盒子都去掉为止。每一轮你可以移除具有相同颜色的连续 k 个盒子(k >= 1),这样一轮之后你将得到 k*k 个积分。当你将所有盒子都去掉之后,求你能获得的最大积分和。

解释:

[1, 3, 2, 2, 2, 3, 4, 3, 1]

----> [1, 3, 3, 4, 3, 1] (3*3=9 分)

----> [1, 3, 3, 3, 1] (1*1=1 分)

----> [1, 1] (3*3=9 分)

----> [] (2*2=4 分)

思路
通过用 dp[i][j][k] 来表示通过移除boxes[i, j]中的箱子,且此时在boxes[i]前有k个箱子的颜色与boxes[i]的颜色相同时,可以获得的最大分数。此时,可以假设boxes数组的长度是n,可以将结果表示为:dp[0][n - 1][0],而且此时有如下的一些初始状态:
 

dp[i][i][k] = (k + 1) * (k + 1)

dp[i][j][k] = 0; //i < j

考虑一般的情况,对于 dp[i][j][k] 而言,考虑如何将其分解成子问题,以通过递推来求解。

上面说到,dp[i][j][k] 表示的是通过移除boxes[i, j]中的箱子,且此时在boxes[i]前面有k个与boxes[i]颜色相同的箱子。因此,对于第i个箱子,如果将其和前面的k个箱子一起移除,那么此时可以获得的分数,可以表示为:

(k + 1) * (k + 1) + dp[i + 1][j][0]

同时对于第i个箱子,还有其他的方案来移除,即可以将boxes[i, j]中的某一个箱子一起移除,这个箱子可以表示为boxes[m],此时boxes[m] == boxes[i]。此时可以获得的分数,可以表示为:

dp[i + 1][m - 1][0] + dp[m][j][k + 1]

而此时的 dp[i][j][k] 就是这些情况下可以取得的最大值。

因此可以写出状态转移方程如下:

temp1 = (k + 1) * (k + 1) + dp[i + 1][j][0]

temp2 = max(dp[i + 1][m - 1][0] + dp[m][j][k + 1]) //i <= m <= j && boxes[m] == boxes[i]

dp[i][j][k] = max(temp1, temp2)
/*DP[i][j][k]表示i-j且后面有k个与j相同的元素;
两种情况 BACAA
第一种,将j后面相同的删除,dp[i][j][k] = dp[i][j-1][0] + (k+1)*(k+1);
第二种,消除中间的部分 BACAA => BAAA + C;这时候就要分断点讨论了
*/
class Solution {
private:
    int mem[101][101][101] ;
public:
    int removeBoxes(vector<int>& boxes) {
        int n = boxes.size();
        memset(mem,0,sizeof(mem));
        return dfs(boxes,0,n-1,0);
    }
    int dfs(vector<int>& boxes ,int l,int r,int k){
        if(l > r) return 0;
        int rr = r;
        int kk = k;
         
        while(l < r && boxes[r] == boxes[r-1]){r--;k++;} 
        if(mem[l][r][k] > 0) return mem[l][r][k];
        int &ans = mem[l][r][k];
        ans = dfs(boxes ,l,r-1,0) + (k+1)*(k+1);
        for(int i = l;i<r ; i++){
            if(boxes[i] == boxes[r]){
                ans = max(ans,dfs(boxes ,l,i,k+1) + dfs(boxes ,i+1,r-1,0));
            }
        }
         
        return ans;
    }
};


 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章