动态规划

线性DP

数字三角形模型

数字三角形

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

数据范围
1≤𝑛≤500
−10000≤三角形中的整数≤10000

解题思路:

#include<iostream>

using namespace std;

const int N = 510, INF = -1e9;
int g[N][N];
int f[N][N];
int n;

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= i; j++)
            cin >> g[i][j];
    
    for(int i = 0; i <= n; i++)
        for(int j = 0; j <= n; j++)
            f[i][j] = INF;
            
    f[1][1] = g[1][1];
    for(int i = 2; i <= n; i++)
        for(int j = 1; j <= i; j++)
            f[i][j] = max(f[i-1][j], f[i-1][j-1]) + g[i][j];
            
    int res = INF;
    for(int i = 1; i <= n; i++)  res = max(res, f[n][i]);
    cout << res << endl;
    return 0;
}

注意:将原本的三角形模型转换之后,下一层的第一个元素和最后一个元素(下一层为 i + 1层)会被本层的(第i层)第一个的前一个和最后一个的后一个干扰到,f[i][0], f[i][i + 1]。为了避免干扰就将数组全部预处理为INF。
还需要注意的一个点就是,本题要求的是顶点到底层的最大路径和,所以说需要遍历最后一层,取最大值

摘花生

题目描述

Hello Kitty想摘点花生送给她喜欢的米老鼠。

她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。

地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。

Hello Kitty只能向东或向南走,不能向西或向北走。

问Hello Kitty最多能够摘到多少颗花生。

输入

第一行是一个整数T,代表一共有多少组数据。

接下来是T组数据。

每组数据的第一行是两个整数,分别代表花生苗的行数R和列数 C。

每组数据的接下来R行数据,从北向南依次描述每行花生苗的情况。每行数据有C个整数,按从西向东的顺序描述了该行每株花生苗上的花生数目M。

输出
对每组输入数据,输出一行,内容为Hello Kitty能摘到得最多的花生颗数

数据范围

1≤𝑇≤100,
1≤𝑅,𝐶≤100,
0≤M≤1000

解题思路

#include<iostream>

using namespace std;

const int N = 110;

int g[N][N], f[N][N];
int n, m;

int main()
{
    int t; cin >> t;
    while(t--)
    {
        cin >> n >> m;
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= m; j++)
                cin >> g[i][j];
                
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= m; j++)
                f[i][j] = max(f[i-1][j], f[i][j-1]) + g[i][j];
        
        cout << f[n][m] << endl;
    }
    return 0;
}

最低通行费

题目描述

一个商人穿过一个 N×N𝑁×𝑁 的正方形的网格,去参加一个非常重要的商务活动。

他要从网格的左上角进,右下角出。

每穿越中间 11 个小方格,都要花费 11 个单位时间。

商人必须在 (2N−1)(2𝑁−1) 个单位时间穿越出去。

而在经过中间的每个小方格时,都需要缴纳一定的费用。

这个商人期望在规定时间内用最少费用穿越出去。

请问至少需要多少费用?

注意:不能对角穿越各个小方格(即,只能向上下左右四个方向移动且不能离开网格)。

输入
第一行是一个整数,表示正方形的宽度 N𝑁。

后面 N𝑁 行,每行 N𝑁 个不大于 100100 的正整数,为网格上每个小方格的费用。

输出
输出一个整数,表示至少需要的费用。

数据范围
1≤N≤100

解题

#include<iostream>

using namespace std;

const int N = 110;

int g[N][N], f[N][N];
int n;

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            cin >> g[i][j];
            
    for(int i = 1; i <= n; i++)  
    {
        f[i][1] = f[i-1][1] + g[i][1];
        f[1][i] = f[1][i-1] + g[1][i];
    }
    
    for(int i = 2; i <= n; i++)
        for(int j = 2; j <= n; j++)
            f[i][j] = min(f[i-1][j], f[i][j-1]) + g[i][j];
            
    cout << f[n][n] << endl;
    return 0;
}

方格取数 

题目描述

设有 N×N 的方格图,我们在其中的某些方格中填入正整数,而其它的方格中则放入数字0。如下图所示:

2.gif

某人从图中的左上角 A 出发,可以向下行走,也可以向右行走,直到到达右下角的 B 点。

在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字0)。

此人从 A 点到 B 点共走了两次,试找出两条这样的路径,使得取得的数字和为最大

输入

第一行为一个整数N,表示 N×N 的方格图。

接下来的每行有三个整数,第一个为行号数,第二个为列号数,第三个为在该行、该列上所放的数。

行和列编号从 11 开始。

一行“0 0 0”表示结束。

输出
输出一个整数,表示两条路径上取得的最大的和。

数据范围
N <= 10

解题
本题和上面三题不同的是,可以走两次,两次取最优解,假如是分成两次用数字三角形模型解题,那么第一次确实可以求到最优解,但是当走过之后会把网格置零,并且不止是将最优解的路径置零,还会将其余的位置也置零,那么就会影响到第二次路径的最优解。所以需要两次同时走,接下来的是同时走的解法。

#include<iostream>

using namespace std;

const int N = 15;

int n;
int g[N][N], f[2*N][N][N];

int main()
{
    cin >> n;
    int a, b, c;
    while(cin >> a >> b >> c && a != 0)
        g[a][b] = c;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            cin >> g[i][j];
        
    for(int k = 2; k <= 2 * n; k ++)
        for(int i1 = 1; i1 <= n; i1++)
            for(int i2 = 1; i2 <= n; i2++)
            {
                int j1 = k - i1, j2 = k - i2;
                if(j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n)  
                // 左边可能不合法,需要特判,i1 和 i2一定合法,所以只需要判断j1  和 j2
                {
                    int t = g[i1][j1];
                    if(i1 != i2) t += g[i2][j2];
                    int& x = f[k][i1][i2];  // 为了简洁,使用引用
                    x = max(x, f[k-1][i1-1][i2-1] + t);
                    x = max(x, f[k-1][i1-1][i2] + t);
                    x = max(x, f[k-1][i1][i2] + t);
                    x = max(x, f[k-1][i1][i2-1] + t);
                }
            }
        
    cout << f[2*n][n][n] << endl;
    return 0;
    
}

最长上升子序列模型

最长上升子序列 I

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

输入

第一行包含整数 N𝑁。

第二行包含 N𝑁 个整数,表示完整序列。

输出
输出一个整数,表示最大长度。

数据范围
1≤𝑁≤1000,
−1e9≤数列中的数≤1e9

题解

#include<iostream>

using namespace std;

const int N = 1010;

int a[N], f[N];
int 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;
        for(int j = 1; j < i; j++)
            if(a[j] < a[i]) 
                f[i] = max(f[i], f[j] + 1);
    }
    
    int res = 0;
    for(int i = 1; i <= n; i++)  res = max(res, f[i]);
    
    cout << res << endl;
    return 0;
}

 最长上升子序列 II

相对于最长上升子序列 I 来说,最长上升子序列 II 只是数据范围扩大了

数据范围
1≤𝑁≤100000,
−1e9≤数列中的数≤1e9

动态规划的解法时间复杂度是 O(N^2),扩大了数据范围之后就会超时,所以考虑O(N logN)的解法

使用贪心,观察数组,求最长上升子序列,在动态规划的思想上转换一下,以i结尾的最长上升子序列的长度是根据i的前一个最长上升子序列得到的,那么观察i前面的数值特征。
不难发现,假如倒数第二个是小于a[i] 的,那么a[i] 一定可以接到它后面。还可以发现,对于两个数,一定是更小的值更优,因为a[i] 可以接到较大的数后面,那么一定可以接到较小的数后面。所以可以再开一个数组,存储上升子序列,这个数组的长度就是最长上升子序列的长度。对于一个数a[i],每次应该接到小于a[i]的最大的数的后面。

#include<iostream>

using namespace std;

const int N = 1e5 + 10;
int n;
int a[N], q[N];

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)  cin >> a[i];
    int len = 0;
    q[0] = -1e9 - 10;
    for(int i = 1; i <= n; i++)
    {
        int l = 0, r = len;
        while(l < r)
        {
            int mid = l + r + 1 >> 1;
            if(q[mid] < a[i])  l = mid;
            else r = mid - 1;
        }
        q[r + 1] = a[i];
        len = max(len, r + 1);
    }
    
    cout << len << endl;
    return 0;
}

最长公共子序列

题目描述
给定两个长度分别为 N𝑁 和 M𝑀 的字符串 A𝐴 和 B𝐵,求既是 A𝐴 的子序列又是 B𝐵 的子序列的字符串长度最长是多少

输入
第一行包含两个整数 N𝑁 和 M𝑀。

第二行包含一个长度为 N𝑁 的字符串,表示字符串 A𝐴。

第三行包含一个长度为 M𝑀 的字符串,表示字符串 B𝐵。

字符串均由小写字母构成。

输出
输出一个整数,表示最大长度。

数据范围
1≤N,M≤1000

解题

#include<iostream>

using namespace std;

const int N = 1010;

int n, m;
int f[N][N];
char a[N], b[N];


int main()
{
    cin >> n >> m;
    scanf("%s %s", a + 1, b + 1);
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
        {
            f[i][j] = max(f[i-1][j], f[i][j-1]);
            f[i][j] = max(f[i][j], f[i-1][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;
}

 怪盗基德的滑翔翼

题目描述
怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。

而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。

有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。

不得已,怪盗基德只能操作受损的滑翔翼逃脱。

假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。

初始时,怪盗基德可以在任何一幢建筑的顶端。

他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。

因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。

他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。

请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?

输入
输入数据第一行是一个整数K,代表有K组测试数据。

每组测试数据包含两行:第一行是一个整数N,代表有N幢建筑。第二行包含N个不同的整数,每一个对应一幢建筑的高度h,按照建筑的排列顺序给出。

输出
对于每一组测试数据,输出一行,包含一个整数,代表怪盗基德最多可以经过的建筑数量。

数据范围
1≤𝐾≤100,
1≤𝑁≤100,
0<h<10000

解题 
题目获取:

  1. 可以选择任意地点滑行。
  2. 只能选择一个方向滑行。
  3. 滑行只能向下降,不可上升

求最多可以经过的建筑数量,可以正序求一遍最长上升子序列,逆序求一遍最长上升子序列,两个序列最长的,就是答案。

#include<iostream>

using namespace std;

const int N = 110;

int n;
int a[N], fl[N], fr[N];

int main()
{
    int t; cin >> t;
    while(t--)
    {
        cin >> n;
        for(int i = 1; i <= n; i++)  cin >> a[i];
        
        for(int i = 1; i <= n; i++)
        {
            fl[i] = 1;
            for(int j = 1; j < i; j++)
                if(a[j] < a[i])
                    fl[i] = max(fl[i], fl[j] + 1);
        }
        
        for(int i = n; i >= 1; i--)
        {
            fr[i] = 1;
            for(int j = n; j > i; j--)
                if(a[j] < a[i])
                    fr[i] = max(fr[i], fr[j] + 1);
        }
        
        int res = 0;
        for(int i = 1; i <= n; i++)
        {
            res = max(res, fl[i]);
            res = max(res, fr[i]);
        }
        
        cout << res << endl;
    }
    return 0;
}

登山

题目描述
五一到了,ACM队组织大家去登山观光,队员们发现山上一共有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。

同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。

队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?

输入
第一行包含整数N,表示景点数量。

第二行包含N个整数,表示每个景点的海拔。

输出
输出一个整数,表示最多能浏览的景点数。

数据范围
2≤N≤1000

题解
题目捕捉:

每次浏览的景点的编号都要大于前一个浏览景点的编号说明:是一个子序列
一旦开始下山,就不再向上走:是一个先递增然后递减(也可单纯的递增,不递减)的子序列。
尽可能多:最长子序列

和上一题一样,先正序求一遍最长上升子序列,再逆序求一遍最长上升子序列。

#include<iostream>

using namespace  std;

const int N = 1010;

int n;
int a[N], fl[N], fr[N];

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)  cin >> a[i];
    
    for(int i = 1; i <= n; i++)
    {
        fl[i] = 1;
        for(int j = 1; j < i; j++)
            if(a[j] < a[i])  fl[i] = max(fl[i], fl[j] + 1);
    }
    
    for(int i = n; i >= 1; i--)
    {
        fr[i] = 1;
        for(int j = n; j > i; j--)
            if(a[j] < a[i])  fr[i] = max(fr[i], fr[j] + 1);
    }
    
    int res = 0;
    for(int i = 1; i <= n; i++)  res = max(res, fl[i] + fr[i] - 1);
    
    cout << res << endl;
    return 0;
    
}

友好城市

题目描述

Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。

北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。

每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。

编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。

输入

第1行,一个整数N,表示城市数。

第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。

输出
仅一行,输出一个整数,表示政府所能批准的最多申请数。

数据范围
1≤𝑁≤5000,
0≤xi≤10000

题解

题目捕捉:
河两岸一一对应的城市之间需要修桥,任意两桥不能相交。问最多可以修多少架桥

先升序排序一边的城市,对另一边的城市求最长上升子序列

#include <iostream>
#include<algorithm>
using namespace std;
#define x first
#define y second

typedef pair<int, int> PII;
const int N = 5010;

int n;
int f[N];
PII a[N];

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)    cin >> a[i].x >> a[i].y;
    
    sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; i++)
    {
        f[i] = 1;
        for (int j = 1; j < i; j++)
            if (a[j].y < a[i].y)    f[i] = max(f[i], f[j] + 1);
    }
    
    int res = 0;
    for(int i = 1; i <= n; i++)  res = max(res, f[i]);
    
    cout << res << endl;
    return 0;
    
}

最大上升子序列和

题目描述

一个数的序列 bi𝑏𝑖,当 b1<b2<…<bS𝑏1<𝑏2<…<𝑏𝑆 的时候,我们称这个序列是上升的。

对于给定的一个序列(a1,a2,…,aN𝑎1,𝑎2,…,𝑎𝑁),我们可以得到一些上升的子序列(ai1,ai2,…,aiK𝑎𝑖1,𝑎𝑖2,…,𝑎𝑖𝐾),这里1≤i1<i2<…<iK≤N1≤𝑖1<𝑖2<…<𝑖𝐾≤𝑁。

比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。

这些子序列中和最大为18,为子序列(1,3,5,9)的和。

你的任务,就是对于给定的序列,求出最大上升子序列和。

注意,最长的上升子序列的和不一定是最大的,比如序列(100,1,2,3)的最大上升子序列和为100,而最长上升子序列为(1,2,3)。

输入

输入的第一行是序列的长度N。

第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。

输出
输出一个整数,表示最大上升子序列和。

数据范围
1≤N≤1000

题解

#include<iostream>

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];
    
    int res = 0;
    for(int i = 1; i <= n; i++)
    {
        f[i] = a[i];
        for(int j = 1; j < i; j++)
            if(a[j] < a[i])
                f[i] = max(f[i], f[j] + a[i]);
        res = max(res, f[i]);
    }
    
    cout << res << endl;
    return 0;
}

拦截导弹

题目描述

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。

但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。

某天,雷达捕捉到敌国的导弹来袭。

由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入

共一行,输入导弹依次飞来的高度。

输出

第一行包含一个整数,表示最多能拦截的导弹数。

第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。

数据范围

雷达给出的高度数据是不大于 30000 的正整数,导弹数不超过 1000。

题解

本题共两问,第一问很明显就是求最长下降(也可以相等)子序列。

那么第二问,可以用贪心。

从前往后每个数,会出现两种情况

  1. 整个子序列的结尾没有大于等于当前数,创建一个新的子序列
  2. 将当前数放在子序列的结尾数中 大于等于当前数的最小的一个结尾数之后。

每当配备一个新的导弹系统,该导弹系统拦截的导弹高度,都可以看成一个子序列。

那么现在需要证明贪心法求出的答案是否等于最优解呢?
不妨设贪心法求出的最少配备系统数为A,最优解为B。
毋庸置疑B <= A,为了 A == B,只需要证明 A <= B 即可(调整法)

友好城市

Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。

北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。

每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。

编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。

输入格式

第1行,一个整数N,表示城市数。

第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。

输出格式

仅一行,输出一个整数,表示政府所能批准的最多申请数。

数据范围

1≤N≤5000,
0≤xi≤10000

题解

题目捕捉:

  1. 每个城市看作一个点,每个点只有一条边
  2. 边不可相交
  3. 目标求 最多可以连多少条边

将一边的点按照编号排序,对于任意的两条不相交的边,另一边的两个点应该是对应点的大小性质,也就是说:另外一边的两个点应该也是有序的。

#include<iostream>
#include<algorithm>
#define x first
#define y second

using namespace std;
typedef pair<int, int> PII;
const int N = 5010;

int n;
PII a[N];
int f[N];

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++) {
        int b, c; cin >> b >> c;
        a[i] = {b, c};
    }
    sort(a + 1, a + n + 1);
    
    for(int i = 1; i <= n; i++) {
        f[i] = 1;
        for(int j = 1; j <= i; j++)
            if(a[j].y < a[i].y) f[i] = max(f[i], f[j] + 1);
    }
    
    cout << *max_element(f + 1, f + n + 1) << endl;
    return 0;
}

背包问题

根据题目对体积的状态规定,可以划分为三类:

  1. 体积最多为j  (对于dp数组的初始化:全部为0,v >= 0)
  2. 体积恰好为j  (对于dp数组的初始化:f[0] = 0,其他为INF,v >= 0)
  3. 体积最少为j  (对于dp数组的初始化:f[0] = 0,其他为INF,这里v的状态可以为负数,为了不让数组越界(max(0, j - v))

01背包

题目描述
有 N件物品和一个容量是 V𝑉 的背包。每件物品只能使用一次。

第 𝑖 件物品的体积是 v𝑖,价值是 𝑤𝑖。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N𝑁 行,每行两个整数 vi,wi𝑣𝑖,𝑤𝑖,用空格隔开,分别表示第 i𝑖 件物品的体积和价值。

输出
输出一个整数,表示最大价值。

数据范围
0<𝑁,𝑉≤1000
0<vi,wi≤1000

题解

未优化的二维代码 

#include<iostream>

using namespace std;

const int N = 1010;

int 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 = 1; 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;
}

一维优化 
 

#include<iostream>

using namespace std;

const int N = 1010;

int n, m;
int f[N];

int main()
{
    cin >> n >> m;
    for(int i = 0; i < n; i++)
    {
        int v, w; cin >> v >> w;
        for(int j = m; j >= v; j--) f[j] = max(f[j], f[j - v] + w);
    }
    
    cout << f[m] << endl;
    return 0;
}

采药

题目描述

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。

为此,他想拜附近最有威望的医师为师。

医师为了判断他的资质,给他出了一个难题。

医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

输入

输入文件的第一行有两个整数 T𝑇 和 M𝑀,用一个空格隔开,T𝑇 代表总共能够用来采药的时间,M𝑀 代表山洞里的草药的数目。

接下来的 M𝑀 行每行包括两个在 11 到 100100 之间(包括 11 和 100100)的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出

输出文件包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。

数据范围
1≤𝑇≤1000,
1≤M≤100

#include<iostream>

using namespace std;

const int N = 1010;

int n, m;
int f[N];

int main()
{
    cin >> m >> n;
    
    for(int i = 0; i < n; i++)
    {
        int v, w; cin >> v >> w;
        for(int j = m; j >= v; j--)
            f[j] = max(f[j], f[j-v] + w);
    }
    
    cout << f[m] << endl;
    return 0;
}

装箱问题

题目描述

有一个箱子容量为 V,同时有 n 个物品,每个物品有一个体积(正整数)。

要求 n 个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。

输入

第一行是一个整数 V,表示箱子容量。

第二行是一个整数 n,表示物品数。

接下来 n 行,每行一个正整数(不超过10000),分别表示这 n 个物品的各自体积。

输出

一个整数,表示箱子剩余空间。

数据范围
0<𝑉≤20000,
0<n≤30

题解

将箱子的体积看成价值,问题转化为有n个物品,已知每个物品的体积和价格,问不超过箱子的容量V,最大价值为多少,那么又是一个01背包问题

#include<iostream>

using namespace std;

const int N = 20010;
int n, m;
int f[N];

int main()
{
    cin >> m >> n;
    for(int i = 0; i < n; i++)
    {
        int v; cin >> v;
        for(int j = m; j >= v; j--)
            f[j] = max(f[j], f[j - v] + v);
    }
    
    cout << m - f[m] << endl;
    return 0;
}

数字组合

题目描述

给定 N𝑁 个正整数 A1,A2,…,AN𝐴1,𝐴2,…,𝐴𝑁,从中选出若干个数,使它们的和为 M𝑀,求有多少种选择方案。

输入

第一行包含两个整数 N𝑁 和 M𝑀。

第二行包含 N𝑁 个整数,表示 A1,A2,…,AN𝐴1,𝐴2,…,𝐴𝑁。

输出

包含一个整数,表示可选方案数。

数据范围

1≤N≤1001≤𝑁≤100,
1≤M≤100001≤𝑀≤10000,
1≤Ai≤10001≤𝐴𝑖≤1000,
答案保证在 int 范围内。

题解

每个数看做体积,去装满体积为m的背包。

#include<iostream>

using namespace std;

const int N = 10010;

int n, m;
int f[N];

int main()
{
    cin >> n >> m;
    f[0] = 1;
    for(int i = 0; i < n; i++)
    {
        int a; cin >> a;
        for(int j = m; j >= a; j--) 
            f[j] += f[j-a];
    }
    
    cout << f[m] << endl;
    return 0;
}

背包问题求具体方案

题目描述

有 N𝑁 件物品和一个容量是 V𝑉 的背包。每件物品只能使用一次。

第 i𝑖 件物品的体积是 vi𝑣𝑖,价值是 wi𝑤𝑖。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N1…𝑁。

输入

第一行两个整数,N,V𝑁,𝑉,用空格隔开,分别表示物品数量和背包容积。

接下来有 N𝑁 行,每行两个整数 vi,wi𝑣𝑖,𝑤𝑖,用空格隔开,分别表示第 i𝑖 件物品的体积和价值。

输出

输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。

物品编号范围是 1…N1…𝑁。

数据范围
0<𝑁,𝑉≤1000
0<vi,wi≤1000

题解

本题要求字典序最小的方案,也就要先输出编号小的。

首先对于01背包的物品选择有三情况:(f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i])

  1. f[i - 1][j] > f[i - 1][j - v[i]] + w[i], 不选
  2. f[i - 1][j] < f[i - 1][j - v[i]] + w[i], 选
  3. f[i - 1][j] = f[i - 1][j - v[i]] + w[i], 可选也可不选

为了字典序最小,编号从小到大遍历时,当出现情况三的时候,也得选。

首先得从编号大到小进行动态规划,求得最大价值,这个时候再从小到大遍历就是正序了。

#include<iostream>

using namespace std;

const int N = 1010;

int n, m;
int f[N][N];
int v[N], w[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)  cin >> v[i] >> w[i];
    
    for(int i = n; i >= 1; 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]);
        }
            
    // f[i - 1][j - v[i]] + w[i] == f[i][j]
    int j = m;
    for(int i = 1; i <= n; i++)
    {
        if(j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i])
        {
            cout << i << " ";
            j -= v[i];  // 没选择一个物品,需要减去该物品的体积
        }
    }
    
    return 0;
    
    
}

背包问题求方案数 

题目描述
有 N 件物品和一个容量是 V𝑉 的背包。每件物品只能使用一次。

第 i𝑖 件物品的体积是 vi𝑣𝑖,价值是 wi𝑤𝑖。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 最优选法的方案数。注意答案可能很大,请输出答案模 109+7109+7 的结果。

输入
第一行两个整数,N,V𝑁,𝑉,用空格隔开,分别表示物品数量和背包容积。

接下来有 N𝑁 行,每行两个整数 vi,wi𝑣𝑖,𝑤𝑖,用空格隔开,分别表示第 i𝑖 件物品的体积和价值。

输出
输出一个整数,表示 方案数 模 109+7109+7 的结果。

数据范围
0<𝑁,𝑉≤1000
0<vi,wi≤1000

题解

运用01背包求最大价值,但是同时还得求最大价值的方案数;根据01背包的状态计算方程

f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w),每一个状态是由前面递推的,那么最优方案数也可以被递推

令g[i][j] 表示在1~i 的物品中选,体积恰好为 j 的方案

当 f[i - 1][j] > f[i - 1][j - v] + w,g[i][j] = g[i - 1][j]
当 f[i - 1][j] < f[i - 1][j - v] + w,g[i][j] = g[i - 1][j - v]
当 f[i - 1][j] == f[i - 1][j - v] + w,g[i][j] = g[i - 1][j] + g[i - 1][j - v]

同样g数组也可以优化为一维

而 f[i][j] 表示在 1~i 的物品中选,体积恰好为 j 的价值。
注意这为了和 g 数组达成同种状态,所以也为恰好。
那么这里的f[n][m] 就不是最大价值了,最大价值需要遍历第n行,求最大值

当求方案数的时候,也不是g[n][m],也需要遍历第n行,累加最大价值对应的方案数

#include<cstring>
#include<iostream>

using namespace std;

typedef long long ll;
const int N = 1010, Mod = 1e9 + 7;

int n, m;
int f[N], g[N];

int main()
{
    cin >> n >> m;
    memset(f, -0x3f, sizeof f);
    f[0] = 0;
    g[0] = 1;
    for(int i = 0; i < n; i++)
    {
        int v, w;  cin >> v >> w;
        
        for(int j = m; j >= v; j--)
        {
            int maxv = max(f[j], f[j - v] + w);
            int cnt = 0;
            if(maxv == f[j]) cnt = (cnt + g[j]) % Mod;
            if(maxv == f[j - v] + w)  cnt = (cnt + g[j - v]) % Mod;
            
            f[j] = maxv, g[j] = cnt;
        }
    }
    
    int res = 0;
    for(int i = 1; i <= m; i++)
        if(f[i] > f[res]) 
            res = i;
            
    int sum = 0;
    for(int i = 0; i <= m; i++)
        if(f[i] == f[res])
            sum = (sum + g[i]) % Mod;
    
    cout << sum << endl;
    return 0;
}

能量石

题目描述

岩石怪物杜达生活在魔法森林中,他在午餐时收集了 N𝑁 块能量石准备开吃。

由于他的嘴很小,所以一次只能吃一块能量石。

能量石很硬,吃完需要花不少时间。

吃完第 i𝑖 块能量石需要花费的时间为 Si𝑆𝑖 秒。

杜达靠吃能量石来获取能量。

不同的能量石包含的能量可能不同。

此外,能量石会随着时间流逝逐渐失去能量。

第 i𝑖 块能量石最初包含 Ei𝐸𝑖 单位的能量,并且每秒将失去 Li𝐿𝑖 单位的能量。

当杜达开始吃一块能量石时,他就会立即获得该能量石所含的全部能量(无论实际吃完该石头需要多少时间)。

能量石中包含的能量最多降低至 00。

请问杜达通过吃能量石可以获得的最大能量是多少?

输入

第一行包含整数 T𝑇,表示共有 T𝑇 组测试数据。

每组数据第一行包含整数 N𝑁,表示能量石的数量。

接下来 N𝑁 行,每行包含三个整数 Si,Ei,Li𝑆𝑖,𝐸𝑖,𝐿𝑖。

输出

每组数据输出一个结果,每个结果占一行。

结果表示为 Case #x: y,其中 x𝑥 是组别编号(从 11 开始),y𝑦 是可以获得的最大能量值。

数据范围
1≤𝑇≤10,
1≤𝑁≤100,
1≤𝑆𝑖≤100,
1≤𝐸𝑖≤105,
0≤Li≤105

题解

先从动态规划的方向思考,状态表示一个集合,这个集合表示所有不同的吃法。

这些吃法从两个维度考虑

  1. 选择一些能量石不吃。(当能量石的能量为0时,吃于不吃是等价的)
  2. 按照什么样的顺序去吃。(不同的顺序得到的能量不同)

按照这两个维度,去将状态集合缩小。

任意选择一种顺序吃能量石,然后在该顺序下任意选择两个相邻的能量石头编号为 i 和 i+1。

考虑在吃这两块能量石之前消耗的能量小于本身的能量,也就是说在吃这两块能量石的时候,保证能量大于0;不妨设在吃第 i 块能量石的时候,能量为E{}'i; 在吃第 i+1 块能量石的时候,能量为E{}'i+1

假如先吃第 i 块能量石,得到的能量为    E{}'i + E{}'i+1 - Si * Li+1
假如先吃第 i+1 块能量石,得到的能量为 E{}'i + E{}'i+1 - Si+1 * Li

若想先吃第 i 块能量石的能量更大,只需Si * Li+1  < Si+1 * Li

所以可以先对能量石排序,这样就可以大大缩小考虑的集合了。

对于得到的最大能量可以使用01背包,每块能量石只能吃一次,时间看作体积,能量看作价值

需要注意的是,这里对于的集合中体积意义应该是:恰好为体积 j。
因为并不是花越多的时间越好。

#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

const int N = 10010;

struct node
{
    int s, e, l;
}nodes[110];

int f[N];
int n;

bool cmp(const node& e1, const node& e2)
{
    return e1.s * e2.l < e2.s * e1.l;
}

int main()
{
    int T; cin >> T;
    for(int C = 1; C <= T; C++)
    {
        memset(f, -0x3f, sizeof f);  // 对于恰好的体积初始化
        f[0] = 0;
        cin >> n;
        int m = 0;
    
        for(int i = 0; i < n; i++)
        {
            int s, e, l;  cin >> s >> e >> l;
            m += s;
            nodes[i] = {s, e, l};
        }
        sort(nodes, nodes + n, cmp);
        for(int i = 0; i < n; i++)
        {
            int s = nodes[i].s;
            int e = nodes[i].e;
            int l = nodes[i].l;
            for(int j = m; j >= s; j--)
            {
                f[j] = max(f[j], f[j - s] + e - (j - s) * l);
            }
        }
        int res = 0;  // 集合表示是恰好,需要遍历取最大价值
        for(int i = 0; i <= m; i++) res = max(res, f[i]);
        
        cout << "Case #" << C << ": " << res << endl;
    }
    return 0;
}

完全背包

题目描述

有 N𝑁 种物品和一个容量是 V𝑉 的背包,每种物品都有无限件可用。

第 i𝑖 种物品的体积是 vi𝑣𝑖,价值是 wi𝑤𝑖。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入

第一行两个整数,N,V𝑁,𝑉,用空格隔开,分别表示物品种数和背包容积。

接下来有 N𝑁 行,每行两个整数 vi,wi𝑣𝑖,𝑤𝑖,用空格隔开,分别表示第 i𝑖 种物品的体积和价值。

输出

输出一个整数,表示最大价值。

数据范围
0<𝑁,𝑉≤1000
0<vi,wi≤1000

题解

未优化的二维代码

#include<iostream>

using namespace std;

const int N = 1010;

int 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 = 1; j <= m; j++)
        {
            f[i][j] = f[i-1][j];
            if(j >= v[i])
                f[i][j] = max(f[i-1][j], f[i][j-v[i]] + w[i]);
        }
    }
    
    cout << f[n][m] << endl;
    return 0;
}

 一维优化

#include<iostream>

using namespace std;

const int N = 1010;

int n, m;
int f[N];

int main()
{
    cin >> n >> m;
    for(int i = 0; i < n; i++)
    {
        int v, w; cin >> v >> w;
        for(int j = v; j <= m; j++)
            f[j] = max(f[j], f[j-v] + w);
    }
    
    cout << f[m] << endl;
    return 0;
}

注意:对于完全背包的一维优化,体积是正序的,根据递推公式,可知f[i][j]的状态还由本层推导

买书

题目描述

小明手里有n元钱全部用来买书,书的价格为10元,20元,50元,100元。

问小明有多少种买书方案?(每种书可购买多本)

输入

小明手里有n元钱全部用来买书,书的价格为10元,20元,50元,100元。

问小明有多少种买书方案?(每种书可购买多本)

输出
一个整数,代表选择方案种数。

数据范围

0≤n≤1000

题解

注意:数组需要初始化,f[0] = 1;解释:当金额为0时,只能买0本书,这只有一种 方案

#include<iostream>

using namespace std;

const int N = 1010;

int n;
int f[N];
int book[] = {0,10, 20, 50, 100};
int main()
{
    cin >> n;
    f[0] = 1;
    for(int i = 1; i <= 4; i++)
        for(int j = book[i]; j <= n; j++)
            f[j] += f[j - book[i]];
        
    cout << f[n] << endl;
    return 0;
}

货币系统I

题目描述

给你一个n种面值的货币系统,求组成面值为m的货币有多少种方案。

输入

第一行,包含两个整数n和m。

接下来n行,每行包含一个整数,表示一种货币的面值。

输出

共一行,包含一个整数,表示方案数。

数据范围

n≤15,m≤3000

题解

一道很裸的完全背包求方案数的模型,注意的点是需要使用long long

#include<iostream>

using namespace std;

const int N = 3010;

int n, m;
long long f[N];

int main()
{
    cin >> n >> m;
    f[0] = 1;
    for(int i = 0; i < n; i++)
    {
        int v;  cin >> v;
        for(int j = v; j <= m; j++)
            f[j] += f[j - v];
    }
    
    cout << f[m] << endl;
    return 0;
}

货币系统II

题目描述

在网友的国度中共有 n𝑛 种不同面额的货币,第 i𝑖 种货币的面额为 a[i]𝑎[𝑖],你可以假设每一种货币都有无穷多张。

为了方便,我们把货币种数为 n𝑛、面额数组为 a[1..n]𝑎[1..𝑛] 的货币系统记作 (n,a)(𝑛,𝑎)。 

在一个完善的货币系统中,每一个非负整数的金额 x𝑥 都应该可以被表示出,即对每一个非负整数 x𝑥,都存在 n𝑛 个非负整数 t[i]𝑡[𝑖] 满足 a[i]×t[i]𝑎[𝑖]×𝑡[𝑖] 的和为 x𝑥。

然而,在网友的国度中,货币系统可能是不完善的,即可能存在金额 x𝑥 不能被该货币系统表示出。

例如在货币系统 n=3, a=[2,5,9]𝑛=3, 𝑎=[2,5,9] 中,金额 1,31,3 就无法被表示出来。 

两个货币系统 (n,a)(𝑛,𝑎) 和 (m,b)(𝑚,𝑏) 是等价的,当且仅当对于任意非负整数 x𝑥,它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。 

现在网友们打算简化一下货币系统。

他们希望找到一个货币系统 (m,b)(𝑚,𝑏),满足 (m,b)(𝑚,𝑏) 与原来的货币系统 (n,a)(𝑛,𝑎) 等价,且 m𝑚 尽可能的小。

他们希望你来协助完成这个艰巨的任务:找到最小的 m𝑚。

输入

输入文件的第一行包含一个整数 T𝑇,表示数据的组数。

接下来按照如下格式分别给出 T𝑇 组数据。 

每组数据的第一行包含一个正整数 n𝑛。

接下来一行包含 n𝑛 个由空格隔开的正整数 a[i]𝑎[𝑖]。

输出

输出文件共有 T𝑇 行,对于每组数据,输出一行一个正整数,表示所有与 (n,a)(𝑛,𝑎) 等价的货币系统 (m,b)(𝑚,𝑏) 中,最小的 m𝑚。

数据范围

1≤𝑛≤100,
1≤𝑎[𝑖]≤25000,
1≤T≤20

输入文件的第一行包含一个整数 T𝑇,表示数据的组数。

接下来按照如下格式分别给出 T𝑇 组数据。 

每组数据的第一行包含一个正整数 n𝑛。

接下来一行包含 n𝑛 个由空格隔开的正整数 a[i]𝑎[𝑖]。

题解

本题需要分析出三个性质:

  1. a1, a2, a3, a4 ..... an 一定都可以被表示出来
  2. b1, b2, b3, b4 ..... bn 一定不能被其他 bi 表示出来
  3. 在最优解中,b1, b2, b3, b4, ..... 一定都是从a1, a2, a3, a4, ..... an中选择的

证明:
第一条性质由题意可知。

第二条性质,由于b集合的个数m需要达到最小,而a集合能表示的b集合都需要能表示,假如b能被除本身之外的b集合内的数表示的话,那么就会产生冗余,m就一定不是最小,与要求矛盾。、

第三条性质,假设  bi 不属于 a 集合,已知 bi 可以被 a 集合中的数表示出来,因为a能表示的b也能表示。
所以不妨设 bi = a1 + a2 + a3; 由于两个集合都为正整数; bi > a1; bi > a2; bi > a3

a集合又可以被b集合表示,所以 a1 = b1 + b2; a2 = b2 + a3; a3 = b3 + b4
bi = b1 + 2*b2 + 2*b3 + b4,这与性质2矛盾了。所以可以通过反证法证明性质3成立。

根据三条性质,可以先将a数组升序排序,因为a都为正整数,所以ai是由比它小的数构成的

这里根据ai能否被构成又分为两种情况:

  1. 能被 a1 到 ai-1 构成, 一定去掉
  2. 否则 , 不能去掉

而如何判断是否能被构成呢?
这里就需要使用到完全背包模型了,将 ai 看成体积。计算方案数。

#include<cstring>
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 110, M = 25010;

int n;
int a[N];
int f[M];

int main()
{
    int t; cin >> t;
    while(t--)
    {
        cin >> n;
        for(int i = 0; i < n; i++) cin >> a[i];
        
        sort(a, a + n);
        int m = a[n - 1];
        int res = 0;
        memset(f, 0, sizeof f);
        f[0] = 1;
        for(int i = 0; i < n; i++)
        {
            if(!f[a[i]]) res ++;   // 对应二维 if(!f[i - 1][a[i]]) res ++; [1,i-1]
            for(int j = a[i]; j <= m; j++)
                f[j] += f[j - a[i]];
            
        }
        
        cout << res << endl;
    }
    return 0;
}

多重背包

题目描述
有 N𝑁 种物品和一个容量是 V𝑉 的背包。

第 i𝑖 种物品最多有 si𝑠𝑖 件,每件体积是 vi𝑣𝑖,价值是 wi𝑤𝑖。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入
第一行两个整数,N,V𝑁,𝑉,用空格隔开,分别表示物品种数和背包容积。

接下来有 N𝑁 行,每行三个整数 vi,wi,si𝑣𝑖,𝑤𝑖,𝑠𝑖,用空格隔开,分别表示第 i𝑖 种物品的体积、价值和数量。

输出
输出一个整数,表示最大价值

数据范围
0<𝑁,𝑉≤100
0<vi,wi,si≤100

多重背包有三个阶段,每一个阶段对应一个解法,我称之为多重背包三重天。

分别是朴素版——> 二进制优化——> 单调队列,滑动窗口优化 。

三个版本对应三个不同的时间复杂度。

 

#include<iostream>

using namespace std;

const int N = 110;

int n, m;
int v[N], w[N], s[N];
int f[N][N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            for(int k = 0; k * v[i] <= j && k <= s[i]; k++)
                f[i][j] = max(f[i][j], f[i-1][j-k*v[i]] + k * w[i]);
                
    cout << f[n][m] << endl;
    return 0;
}

 这个朴素版的时间复杂度为 O(N * V * S)

当数据范围扩大,那么就会TLE
0<𝑁≤1000
0<𝑉≤2000
0<vi,wi,si≤2000
对于上面这个数据范围,明显就会TLE

那么就需要优化,第二重,二进制优化

#include<iostream>

using namespace std;

const int N = 25000, M = 2010;

int n, m;
int v[N], w[N];
int f[M];

int main()
{
    cin >> n >> m;
    int cnt = 0;
    for(int i = 1; i <= n; i++)
    {
        int a, b, s;  cin >> a >> b >> s;
        int k = 1;
        while(k < s)
        {
            cnt++;
            v[cnt] = k * a;
            w[cnt] = k * b;
            s -= k;
            k *= 2;
        }
        if(s > 0)
        {
            cnt++;
            v[cnt] = s * a;
            w[cnt] = s * b;
        }
    }
    
    n = cnt;
    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;
}

 二进制优化之后,会增加数组v和w的长度,注意f数组不可以使用二维,会爆。
log2000 约为11,物品个数为11 * 1000,如果为二维,f数组空间为 11 * 1000 * 2000,2e7

时间复杂度为O(v * n * log s)

使用单调队列优化

#include<iostream>
#include<cstring>

using namespace std;

const int N = 1010, M = 20010;

int f[M], g[M], q[M];
int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 0; i < n; i++)
    {
        memcpy(g, f, sizeof f);  // 使用一维数组优化的话,需要一个备用数组保存上一层,避免被覆盖
        int v, w, s;  cin >> v >> w >> s;
        for(int j = 0; j < v; j++)
        {
            int hh = 0, tt = -1;
            for(int k = j; k <= m; k += v)
            {
                if(hh <= tt && q[hh] < k - s * v) hh++;  // [k - s * v, k - v]
                
//while(hh < tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) 
// 下面是等价于上面的,下面更简洁
                while(hh <= tt && g[k] >= g[q[tt]] + (k - q[tt]) / v * w)
                    tt--;
                q[++tt] = k;
                f[k] = g[q[hh]] + (k - q[hh]) / v * w;
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}

分组背包

题目描述

有 N𝑁 组物品和一个容量是 V𝑉 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij𝑣𝑖𝑗,价值是 wij𝑤𝑖𝑗,其中 i𝑖 是组号,j𝑗 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值

输入

第一行有两个整数 N,V𝑁,𝑉,用空格隔开,分别表示物品组数和背包容量。

接下来有 N𝑁 组数据:

  • 每组数据第一行有一个整数 Si𝑆𝑖,表示第 i𝑖 个物品组的物品数量;
  • 每组数据接下来有 Si𝑆𝑖 行,每行有两个整数 vij,wij𝑣𝑖𝑗,𝑤𝑖𝑗,用空格隔开,分别表示第 i𝑖 个物品组的第 j𝑗 个物品的体积和价值;

输出
输出一个整数,表示最大价值。

数据范围
0<𝑁,𝑉≤100
0<𝑆𝑖≤100
0<𝑣𝑖𝑗,𝑤𝑖𝑗≤100

#include<iostream>

using namespace std;

const int N = 110;

int n, m;
int f[N];
int v[N][N], w[N][N], s[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> s[i];
        for(int k = 1; k <= s[i]; k++)
            cin >> v[i][k] >> w[i][k];
    }
    
    for(int i = 1; i <= n; i++)
    {
        for(int j = m; j >= 0; j--)
        {
            for(int k = 1; k <= s[i]; k++)
            {
                if(j >= v[i][k])
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
            }
        }
    }
    
    cout << f[m] << endl;
    return 0;
}

机器分配

题目描述

总公司拥有 M𝑀 台 相同 的高效设备,准备分给下属的 𝑁 个分公司。

各分公司若获得这些设备,可以为国家提供一定的盈利。盈利与分配的设备数量有关。

问:如何分配这M台设备才能使国家得到的盈利最大?

求出最大盈利值。

分配原则:每个公司有权获得任意数目的设备,但总台数不超过设备数 M𝑀。

输入

第一行有两个数,第一个数是分公司数 N𝑁,第二个数是设备台数 M𝑀;

接下来是一个 N×M𝑁×𝑀 的矩阵,矩阵中的第 i𝑖 行第 j𝑗 列的整数表示第 i𝑖 个公司分配 j𝑗 台机器时的盈利。

输出

第一行输出最大盈利值;

接下 N𝑁 行,每行有 22 个数,即分公司编号和该分公司获得设备台数。

答案不唯一,输出任意合法方案即可。

数据范围

1≤𝑁≤10,
1≤M≤15

题解
有多个子公司,每一个子公司可以接受[0, m] 的机器,数量不同的机器对应不同的价值,每一个子公司可以看成分组背包中的一组

在分组背包中,每一组只能选择一个物品,而在本题中,每一个子公司也只可以选择一个数量的机器,将机器的数量看成体积,对应创造的盈利看成价值。那么就可以完美转化成一个分组背包问题
而第二问是求方案,这里需要注意的是假如是正序求得最大价值,那么就需要逆序求方案。

遍历每一组物品,在遍历每一组中的可选择的数量,选择之后需要break,因为分组背包中,每一组只能选择一个物品

#include<iostream>

using namespace std;

const int N = 15, M = 20;
int n, m;
int w[N][M];
int f[N][M];
int way[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> w[i][j];
            
    for(int i = n; i >= 1; i--)
        for(int j = 0; j <= m; j++)
        {
            f[i][j] = f[i + 1][j];
            for(int k = 1; k <= m; k++)
                if(j >= k)
                    f[i][j] = max(f[i][j], f[i + 1][j - k] + w[i][k]);
        }
                
                    
    cout << f[1][m] << endl;
    
    int j = m;
    for(int i = 1; i <= n; i++)
    {
        for(int k = 0; k <= m; k++)
        {
            if(j >= k && f[i][j] == f[i + 1][j - k] + w[i][k])
            {
                way[i] = k;
                j -= k;
                break;
            }
        }
    }
    
    for(int i = 1; i <= n; i++) cout << i << " " <<  way[i] << endl;
    return 0;
}

金明的预算方案

题目描述

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。

更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 N𝑁 元钱就行”。

今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

QQ截图20190313024710.png

如果要买归类为附件的物品,必须先买该附件所属的主件。

每个主件可以有0个、1个或2个附件。

附件不再有从属于自己的附件。

金明想买的东西很多,肯定会超过妈妈限定的 N𝑁 元。

于是,他把每件物品规定了一个重要度,分为5等:用整数1~5表示,第5等最重要。

他还从因特网上查到了每件物品的价格(都是10元的整数倍)。

他希望在不超过 N𝑁 元(可以等于 N𝑁 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第j件物品的价格为 v[j]𝑣[𝑗],重要度为 w[j]𝑤[𝑗],共选中了 k𝑘 件物品,编号依次为j1,j2,…,jk𝑗1,𝑗2,…,𝑗𝑘,则所求的总和为:

v[j1]∗w[j1]+v[j2]∗w[j2]+…+v[jk]∗w[jk]𝑣[𝑗1]∗𝑤[𝑗1]+𝑣[𝑗2]∗𝑤[𝑗2]+…+𝑣[𝑗𝑘]∗𝑤[𝑗𝑘](其中*为乘号)

请你帮助金明设计一个满足要求的购物单。

输入

输入文件的第 11 行,为两个正整数,用一个空格隔开:N,m𝑁,𝑚,其中 N𝑁 表示总钱数,m𝑚 为希望购买物品的个数。

从第 22 行到第 m+1𝑚+1 行,第 j𝑗 行给出了编号为 j−1𝑗−1 的物品的基本数据,每行有 33 个非负整数 v,p,q𝑣,𝑝,𝑞,其中 v𝑣 表示该物品的价格,p𝑝 表示该物品的重要度(1~5),q𝑞 表示该物品是主件还是附件。

如果 q=0𝑞=0,表示该物品为主件,如果 q>0𝑞>0,表示该物品为附件,q𝑞 是所属主件的编号。

输出

输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<200000)。

数据范围

N<32000,m<60,v<10000

题解

将主件和附件看作一组,题目给出范围一个主件最多有两个附件,所以可以暴力组合方式,一个就四种,并且这四种组合互斥,那么每一组只能选择一种;将金额看着体积,那么本题就可以转化成一个分组背包问题了

在计算每一组的组合情况的时候,有一个二进制优化需要注意。

#include<iostream>
#include<vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 32010, M = 65;

int n, m;
int f[N];
vector<PII> slaver[M];
PII master[M];

int main()
{
    cin >> m >> n;
    for(int i = 1; i <= n; i++)
    {
        int v, p, q; cin >> v >> p >> q;
        if(!q) master[i] = {v, v * p};
        else slaver[q].push_back({v, v * p});
    }
    
    for(int i = 1; i <= n; i++)
    {
        for(int j = m; j >= 0; j--)
        {
            for(int u = 0; u < 1 << slaver[i].size(); u++)
            {
                int v = master[i].first, w = master[i].second;
                for(int k = 0; k < slaver[i].size(); k++)
                {
                    if(u >> k & 1)
                    {
                        v += slaver[i][k].first;
                        w += slaver[i][k].second;
                    }
                }
                if(j >= v) f[j] = max(f[j], f[j - v] + w);
            }
        }
    }
    
    cout << f[m] << endl;
    return 0;
}

下面就是二进制优化,对于每一组的四种情况,分别计算动态规划求最大价值 

for(int u = 0; u < 1 << slaver[i].size(); u++)
            {
                int v = master[i].first, w = master[i].second;
                for(int k = 0; k < slaver[i].size(); k++)
                {
                    if(u >> k & 1)
                    {
                        v += slaver[i][k].first;
                        w += slaver[i][k].second;
                    }
                }
                if(j >= v) f[j] = max(f[j], f[j - v] + w);
            }

二维费用背包问题

题目描述

有 N𝑁 件物品和一个容量是 V𝑉 的背包,背包能承受的最大重量是 M𝑀。

每件物品只能用一次。体积是 vi𝑣𝑖,重量是 mi𝑚𝑖,价值是 wi𝑤𝑖。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。

输入

第一行三个整数,N,V,M𝑁,𝑉,𝑀,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。

接下来有 N𝑁 行,每行三个整数 vi,mi,wi𝑣𝑖,𝑚𝑖,𝑤𝑖,用空格隔开,分别表示第 i𝑖 件物品的体积、重量和价值。

输出

输出一个整数,表示最大价值。

数据范围
0<N≤10000<𝑁≤1000
0<V,M≤1000<𝑉,𝑀≤100
0<vi,mi≤1000<𝑣𝑖,𝑚𝑖≤100
0<wi≤1000

题解

#include<iostream>

using namespace std;

const int N = 110;

int n, m, k;
int f[N][N];

int main()
{
    cin >> n >> m >> k;
    for(int i = 0; i < n; i++)
    {
        int a, b, c;  cin >> a >> b >> c;
        for(int j = m; j >= a; j--)
            for(int z = k; z >= b; z--)
                f[j][z] = max(f[j][z], f[j - a][z - b] + c);
    }
    
    cout << f[m][k] << endl;
    return 0;
}

宠物小精灵之收服

题目描述

宠物小精灵是一部讲述小智和他的搭档皮卡丘一起冒险的故事。

一天,小智和皮卡丘来到了小精灵狩猎场,里面有很多珍贵的野生宠物小精灵。

小智也想收服其中的一些小精灵。

然而,野生的小精灵并不那么容易被收服。

对于每一个野生小精灵而言,小智可能需要使用很多个精灵球才能收服它,而在收服过程中,野生小精灵也会对皮卡丘造成一定的伤害(从而减少皮卡丘的体力)。

当皮卡丘的体力小于等于0时,小智就必须结束狩猎(因为他需要给皮卡丘疗伤),而使得皮卡丘体力小于等于0的野生小精灵也不会被小智收服。

当小智的精灵球用完时,狩猎也宣告结束。

我们假设小智遇到野生小精灵时有两个选择:收服它,或者离开它。

如果小智选择了收服,那么一定会扔出能够收服该小精灵的精灵球,而皮卡丘也一定会受到相应的伤害;如果选择离开它,那么小智不会损失精灵球,皮卡丘也不会损失体力。

小智的目标有两个:主要目标是收服尽可能多的野生小精灵;如果可以收服的小精灵数量一样,小智希望皮卡丘受到的伤害越小(剩余体力越大),因为他们还要继续冒险。

现在已知小智的精灵球数量和皮卡丘的初始体力,已知每一个小精灵需要的用于收服的精灵球数目和它在被收服过程中会对皮卡丘造成的伤害数目。

请问,小智该如何选择收服哪些小精灵以达到他的目标呢?

输入

输入数据的第一行包含三个整数:N,M,K,分别代表小智的精灵球数量、皮卡丘初始的体力值、野生小精灵的数量。

之后的K行,每一行代表一个野生小精灵,包括两个整数:收服该小精灵需要的精灵球的数量,以及收服过程中对皮卡丘造成的伤害。

输出
输出为一行,包含两个整数:C,R,分别表示最多收服C个小精灵,以及收服C个小精灵时皮卡丘的剩余体力值最多为R。

数据范围
0<𝑁≤1000,
0<𝑀≤500,
0<K≤100

题解

本题可转化为二维费用背包问题,一维是精灵球的数量,一维是皮卡丘的体力。
状态表示:f[i][j][k]:捕捉在 1~i 的精灵,使用的精灵球得数量最多为j,消耗皮卡丘的体力最多为k的集合。需要注意的是:当皮卡丘的体力剩余0或小于0时,这个精灵并不会被成功捕捉,所以皮卡丘的体力消耗最多为 M - 1
属性:max。 
集合可分为包含第i个精灵,和不包含第i个精灵。

在求解第二问,皮卡丘剩余的最大体力时,前提是捕捉的精灵也应为最大。

#include<iostream>

using namespace std;

const int N = 1010, M = 510;

int n, m, k;
int f[N][M];

int main()
{
    cin >> n >> m >> k;
    while(k--)
    {
        int a, b; cin >> a >> b;
        for(int j = n; j >= a; j--)
            for(int z = m - 1; z >= b; z--)
                f[j][z] = max(f[j][z], f[j - a][z - b] + 1);
    }
    
    cout << f[n][m - 1] << " ";
    
    int a = m - 1;
    while(a > 0 && f[n][m - 1] == f[n][a - 1]) a--;
    cout << m - a << endl;
    return 0;
}

潜水员

题目描述

潜水员为了潜水要使用特殊的装备。

他有一个带2种气体的气缸:一个为氧气,一个为氮气。

让潜水员下潜的深度需要各种数量的氧和氮。

潜水员有一定数量的气缸。

每个气缸都有重量和气体容量。

潜水员为了完成他的工作需要特定数量的氧和氮。

他完成工作所需气缸的总重的最低限度的是多少?

例如:潜水员有5个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量:

3 36 120

10 25 129

5 50 250

1 45 130

4 20 119

如果潜水员需要5升的氧和60升的氮则总重最小为249(1,2或者4,5号气缸)。

你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。

输入

第一行有2个整数 m,n𝑚,𝑛。它们表示氧,氮各自需要的量。

第二行为整数 k𝑘 表示气缸的个数。

此后的 k𝑘 行,每行包括ai,bi,ci𝑎𝑖,𝑏𝑖,𝑐𝑖,3个整数。这些各自是:第 i𝑖 个气缸里的氧和氮的容量及气缸重量。

输出
仅一行包含一个整数,为潜水员完成工作所需的气缸的重量总和的最低值。

数据范围

1≤𝑚≤21,
1≤𝑛≤79,
1≤𝑘≤1000,
1≤𝑎𝑖≤21,
1≤𝑏𝑖≤79,
1≤ci≤800

题解

#include<cstring>
#include<iostream>

using namespace std;

const int N = 22, M = 80;

int v1, v2, n;
int f[N][M];

int main()
{
    cin >> v1 >> v2 >> n;
    memset(f, 0x3f, sizeof f);
    f[0][0] = 0;
    for(int i = 0; i < n; i++)
    {
        int a, b, c;  cin >> a >> b >> c;
        for(int j = v1; j >= 0; j --)
            for(int k = v2; k >= 0; k--)
                f[j][k] = min(f[j][k], f[max(0, j - a)][max(0, k - b)] + c);
    }
    
    cout << f[v1][v2] << endl;
    return 0;
}

状态机模型

状态机模型和背包模型的区别:
背包模型中,每一个物品是否被选择,并不会影响到其他的物品是否被选择
状态机模型中,一个物品是否被选择,会影响到一些物品是否被选择。

大盗阿福

题目描述
阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。

这条街上一共有 N𝑁 家店铺,每家店中都有一些现金。

阿福事先调查得知,只有当他同时洗劫了两家相邻的店铺时,街上的报警系统才会启动,然后警察就会蜂拥而至。

作为一向谨慎作案的大盗,阿福不愿意冒着被警察追捕的风险行窃。

他想知道,在不惊动警察的情况下,他今晚最多可以得到多少现金?

输入

输入的第一行是一个整数 T𝑇,表示一共有 T𝑇 组数据。

接下来的每组数据,第一行是一个整数 N𝑁 ,表示一共有 N𝑁 家店铺。

第二行是 N𝑁 个被空格分开的正整数,表示每一家店铺中的现金数量。

每家店铺中的现金数量均不超过1000。

输出

对于每组数据,输出一行。

该行包含一个整数,表示阿福在不惊动警察的情况下可以得到的现金数量。

数据范围
1≤𝑇≤50,
1≤N≤1e5

题解

#include<cstring>
#include<iostream>

using namespace std;

const int N = 1e5 + 10;

int n;
int w[N];
int f[N][2];

int main()
{
    int T; cin >> T;
    while(T--)
    {
        cin >> n;
        f[0][1] = -0x3f3f3f3f;  // 这个状态不合法,因为没有房子时,偷不了
        f[0][0] = 0;  // 一个房子都不偷,所以为 0
        for(int i = 1; i <= n; i++)  cin >> w[i];
        
        for(int i = 1; i <= n; i++)
        {
            f[i][0] = max(f[i - 1][0], f[i - 1][1]);
            f[i][1] = f[i - 1][0] + w[i];
        }
        
        cout << max(f[n][1], f[n][0]) << endl;
    }
    return 0;
}

 股票买卖I

题目描述

给定一个长度为 N𝑁 的数组,数组中的第 i𝑖 个数字表示一个给定股票在第 i𝑖 天的价格。

设计一个算法来计算你所能获取的最大利润,你最多可以完成 k𝑘 笔交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易。

输入

第一行包含整数 N𝑁 和 k𝑘,表示数组的长度以及你可以完成的最大交易笔数。

第二行包含 N𝑁 个不超过 1000010000 的非负整数,表示完整的数组。

输出

输出一个整数,表示最大利润。

数据范围
1≤𝑁≤105,
1≤k≤100

题解

还需要注意的是初始化,对于所有不合法的全部初始化为INF 

#include<iostream>
#include<cstring>
using namespace std;

const int N = 1e5 + 10, M = 110, INF = 0x3f3f3f3f;

int f[N][M][2];
int n, k;
int w[N];

int main()
{
    cin >> n >> k;
    for(int i = 1; i <= n; i++)  cin >> w[i];
    
    //memset(f, -0x3f, sizeof f);
    for(int i = 0; i <= k; i++) f[0][i][1] = -INF;
    for(int i = 0; i <= n; i++) f[i][0][0] = 0;
    
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= k; j++)
        {
            f[i][j][0] = max(f[i-1][j][0], f[i-1][j][1] + w[i]);
            f[i][j][1] = max(f[i-1][j][1], f[i-1][j-1][0] - w[i]);
        }
    }
    
    int res = 0;
    for(int i = 0; i <= k; i++) res = max(res, f[n][i][0]);
    
    cout << res << endl;
    return 0;
}

 股票买卖II

题目描述

给定一个长度为 N𝑁 的数组,数组中的第 i𝑖 个数字表示一个给定股票在第 i𝑖 天的价格。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 11 天)。

输入

第一行包含整数 N𝑁,表示数组长度。

第二行包含 N𝑁 个不超过 1000010000 的正整数,表示完整的数组。

输出

输出一个整数,表示最大利润。

数据范围

1≤N≤1e5

题解

#include<iostream>
using namespace std;

const int N = 1e5 + 10;

int n;
int w[N], f[N][3];

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)  cin >> w[i];
    f[0][0] = f[0][1] = -0x3f3f3f3f;  
    f[0][2] = 0;
    for(int i = 1; i <= n; i++)
    {
        f[i][0] = max(f[i - 1][0], f[i - 1][2] - w[i]);
        f[i][1] = f[i - 1][0] + w[i];
        f[i][2] = max(f[i - 1][2], f[i - 1][1]);
    }
    
    cout << max(f[n][1], f[n][2]) << endl;
    return 0;
}

状态压缩

蒙德里安的梦想

求把 N×M𝑁×𝑀 的棋盘分割成若干个 1×21×2 的长方形,有多少种方案。

例如当 N=2,M=4𝑁=2,𝑀=4 时,共有 55 种方案。当 N=2,M=3𝑁=2,𝑀=3 时,共有 33 种方案。

如下图所示:

2411_1.jpg

输入格式

输入包含多组测试用例。

每组测试用例占一行,包含两个整数 N𝑁 和 M𝑀。

当输入用例 N=0,M=0𝑁=0,𝑀=0 时,表示输入终止,且该用例无需处理。

输出格式

每个测试用例输出一个结果,每个结果占一行。

数据范围

1≤N,M≤11

题解

这题的一个核心思想就是先放横着的再放竖着的。

总方案数等于只放横着的方案数,竖着的方块就放在每一种横着放的方案空的网格中

如何判断竖着和横着的方块是否合法?
在所有剩余的空格中,可以按照列来观察;每一列是否满足不存在连续的奇数个空格。

动态规划:集合:f[i][j] 表示 前i列已经放好了,且从第i-1列,伸出到第i列的所有方案,伸到第i列的状态为j。

状态计算,对于当前的第i列,一共有2^n种状态伸过来,每一种状态的方案数,需要累加上一列的与当前列的合法状态。

判断是否存在连续个奇数空格,可以预处理。对于列中可能出现的每一种情况,预处理该情况的空格数是否存在奇数个空格。(每一列中可能出现的情况可以使用二进制表示,0表示为空格)

还需要预处理当前列的状态应该和哪些状态构成合法方案。

#include<iostream>
#include<vector>
#include<cstring>

using namespace std;
typedef long long ll;

const int N = 12, M = 1 << N;

int n, m;
ll f[N][M];
vector<int> state[M];
bool st[M];


int main()
{
    while(cin >> n >> m, n || m)
    {
        // 预处理[0, 1 << n - 1] 区间的二进制中是否存在 不是连续偶数个空位
        for(int i = 0; i < 1 << n; i++)
        {
            int cnt = 0;
            bool is_valid = true;
            for(int j = 0; j < n; j++)
            {
                if(i >> j & 1)
                {
                    if(cnt & 1)
                    {
                        is_valid = false;
                        break;
                    }
                }
                else cnt++;
            }
            if (cnt & 1) is_valid = false;
            st[i] = is_valid;
        }
        
        // 预处理与 i 合法的 状态 j
        for(int i = 0; i < 1 << n; i++)
        {
            state[i].clear();
            for(int j = 0; j < 1 << n; j++)
            {
                if((i & j) == 0 && st[i | j])
                    state[i].push_back(j);
            }
        }
        
        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(auto k : state[j])
                    f[i][j] += f[i - 1][k];
                
        cout << f[m][0] << endl;
    }
    return 0;
}

最后的答案是f[m][0],表示前m列已经放好了,并且前m-1列每一方格伸到第m列。表示这个网格已经全部放好了

最短Hamilton路径

给定一张 n𝑛 个点的带权无向图,点从 0∼n−10∼𝑛−1 标号,求起点 00 到终点 n−1𝑛−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 00 到 n−1𝑛−1 不重不漏地经过每个点恰好一次。

输入格式

第一行输入整数 n𝑛。

接下来 n𝑛 行每行 n𝑛 个整数,其中第 i𝑖 行第 j𝑗 个整数表示点 i𝑖 到 j𝑗 的距离(记为 a[i,j]𝑎[𝑖,𝑗])。

对于任意的 x,y,z𝑥,𝑦,𝑧,数据保证 a[x,x]=0,a[x,y]=a[y,x]𝑎[𝑥,𝑥]=0,𝑎[𝑥,𝑦]=𝑎[𝑦,𝑥] 并且 a[x,y]+a[y,z]≥a[x,z]𝑎[𝑥,𝑦]+𝑎[𝑦,𝑧]≥𝑎[𝑥,𝑧]。

输出格式

输出一个整数,表示最短 Hamilton 路径的长度。

数据范围

1≤n≤201≤𝑛≤20
0≤a[i,j]≤107

题解

状态表示:f[i][j]表示当前到了点j,且一共途径了i(i是一个二进制,1表示经过),的最短距离

状态计算:从最后一步看,观察倒数第二个点,从倒数第二个点移动过来的。

i首先需要包括j这一点,也需要包括倒数第二个点

#include<iostream>
#include<cstring>

using namespace std;

const int N = 20, M = 1 << 20;

int n;
int f[M][N];
int w[N][N];

int main()
{
    cin >> n;
    for(int i = 0; i < n; i++)
        for(int j = 0; j < n; j++)
            cin >> w[i][j];
            
    memset(f, 0x3f, sizeof f);
    f[1][0] = 0;
    
    for(int i = 1; i < 1 << n; i++)
        for(int j = 1; j < n; j++)
            if(i >> j & 1)
                for(int k = 0; k < n; k++)
                    if((i - (1 << j)  >> k) & 1)
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
        
    cout << f[(1 << n) - 1][n - 1] << endl;
    return 0;
}

小国王

在 n×nn×n 的棋盘上放 kk 个国王,国王可攻击相邻的 88 个格子,求使它们无法互相攻击的方案总数。

输入格式

共一行,包含两个整数 nn 和 kk。

输出格式

共一行,表示方案总数,若不能够放置则输出00。

数据范围

1≤n≤10,
0≤k≤n2

题解

动态规划:集合:f[i][j][k] 表示当前已经放到第i行,已经放了j个国王,且当前第i行放入国王的状态是k 的方案数

状态计算:第i行的状态只会和i-1和i+1行的状态相关,现在从前到后遍历,就只会和第i-1行相关。
设当前行的状态表示二进制数为a,上一行的二进制表示为b,那么(a & b) == 0 且 (a | b)不存在两个连续的1

为了优化时间复杂度,需要预处理合法状态和每一个状态下的国王数量(二进制1的数量)

#include<iostream>
#include<vector>

using namespace std;
typedef long long ll;

const int N = 12, M = 1 << 10, K = 110;
int n, m;
ll f[N][K][M];
vector<int> state;
vector<int> head[M];
int cnt[M];

bool check(int state)
{
    for(int i = 0; i < n; i++)
        if((state >> i) & 1 && (state >> (i + 1) & 1))
            return false;
    return true;
}

int count(int state)
{
    int res = 0;
    for(int i = 0; i < n; i++) res += state >> i & 1;
    return res;
    
}

int main()
{
    cin >> n >> m;
    // 预处理合法状态和每一个状态1的数量
    for(int i = 0; i < 1 << n; i++)
        if(check(i))
        {
            state.push_back(i);
            cnt[i] = count(i);
        }
    // 预处理当前行和上一行的合法状态
    for(int i = 0; i < state.size(); i++)
        for(int j = 0; j < state.size(); j++)
        {
            int a = state[i], b = state[j];
            if((a & b) == 0 && check(a | b))
                head[i].push_back(j);    
        }
        
    f[0][0][0] = 1;
    for(int i = 1; i <= n + 1; i++)
        for(int j = 0; j <= m; j++)
            for(int a = 0; a < state.size(); a++)
                for(auto b : head[a])  // head[a] 存储的就是所有与当前行状态a,合法的状态
                {
                    int c = cnt[state[a]];
                    if(j >= c)  
                        f[i][j][a] += f[i - 1][j - c][b];
                }
    // 表示 已经放好了n+1 行,且一共放了m个国王,且当前行的状态是0,即n+1行一个国王也没有
    cout << f[n + 1][m][0] << endl;
    return 0;
}

区间DP

石子合并

设有 N𝑁 堆石子排成一排,其编号为 1,2,3,…,N1,2,3,…,𝑁。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N𝑁 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 44 堆石子分别为 1 3 5 2, 我们可以先合并 1、21、2 堆,代价为 44,得到 4 5 2, 又合并 1、21、2 堆,代价为 99,得到 9 2 ,再合并得到 1111,总代价为 4+9+11=244+9+11=24;

如果第二步是先合并 2、32、3 堆,则代价为 77,得到 4 7,最后一次合并代价为 1111,总代价为 4+7+11=224+7+11=22。

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

输入格式

第一行一个数 N𝑁 表示石子的堆数 N𝑁。

第二行 N𝑁 个数,表示每堆石子的质量(均不超过 10001000)。

输出格式

输出一个整数,表示最小代价。

数据范围

1≤N≤300

题解

动态规划:集合:f[l][r]:合并区间[l, r] 中的石头,需要的最小代价

状态计算:在区间[l, r] 中,可以枚举分界点k,将该区间分为[l, k] 和 [k + 1, r] 两部分,那么状态转移计算公式为:f[l][k] + f[k + 1][r] + pre[r] - pre[l - 1]。 合并左区间的最小代价和合并右区间的最小代价以及合并两个区间需要的代价(这里使用前缀和)。

#include<iostream>
#include<cstring>
using namespace std;

const int N = 310, INF = 1e9;

int n;
int w[N];
int f[N][N];
int pre[N];

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> w[i];
        pre[i] = pre[i - 1] + w[i];
    }
    
    for(int len = 2; len <= n; len++)
        for(int l = 1; l + len - 1 <= n; l++)
        {
            int r = l + len - 1;
            f[l][r] = INF;
            for(int k = l; k < r; k++)
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + pre[r] - pre[l - 1]);
        }
    
    cout << f[1][n] << endl;
    return 0;
}

 环形石子合并

将 nn 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。

规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。

请编写一个程序,读入堆数 nn 及每堆的石子数,并进行如下计算:

  • 选择一种合并石子的方案,使得做 n−1n−1 次合并得分总和最大。
  • 选择一种合并石子的方案,使得做 n−1n−1 次合并得分总和最小。

输入格式

第一行包含整数 nn,表示共有 nn 堆石子。

第二行包含 nn 个整数,分别表示每堆石子的数量。

输出格式

输出共两行:

第一行为合并得分总和最小值,

第二行为合并得分总和最大值。

数据范围

1≤n≤200

题解

这一题是上一题的扩展,将环转为线性的区间DP。只需要将数组复制一份追加到后面即可。在这一2n长度的区间内,计算每一段区间为n的答案,最后枚举答案即可。

#include<iostream>
#include<cstring>

using namespace std;

const int N = 410, INF = 0x3f3f3f3f;

int n;
int w[N], pre[N];
int f[N][N], g[N][N];

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> w[i];
        w[i + n] = w[i];
    }
    for(int i = 1; i <= 2 * n; i++) pre[i] = pre[i - 1] + w[i];
    
    for(int len = 2; len <= n; len++)
    {
        for(int l = 1; l + len - 1 <= 2 * n; l ++)
        {
            int r = l + len - 1;
            f[l][r] = INF;
            g[l][r] = -INF;
                for(int k = l; k < r; k++)
                {
                    f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + pre[r] - pre[l - 1]);
                    g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + pre[r] - pre[l - 1]);
                }
            
        }
    }
    
    int minv = INF, maxv = -INF;
    for(int i = 1; i <= n; i++)
    {
        minv = min(minv, f[i][i + n - 1]);
        maxv = max(maxv, g[i][i + n - 1]);
    }
    
    cout << minv << endl << maxv << endl;
    return 0;
} 

能量项链

在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链,在项链上有 NN 颗能量珠。

能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。

并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。

因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。

如果前一颗能量珠的头标记为 mm,尾标记为 rr,后一颗能量珠的头标记为 rr,尾标记为 nn,则聚合后释放的能量为 m×r×nm×r×n(Mars 单位),新产生的珠子的头标记为 mm,尾标记为 nn。

需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。

显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设 N=4N=4,44 颗珠子的头标记与尾标记依次为 (2,3)(3,5)(5,10)(10,2)(2,3)(3,5)(5,10)(10,2)。

我们用记号 ⊕⊕ 表示两颗珠子的聚合操作,(j⊕k)(j⊕k) 表示第 jj,kk 两颗珠子聚合后所释放的能量。则

第 4、14、1 两颗珠子聚合后释放的能量为:(4⊕1)=10×2×3=60(4⊕1)=10×2×3=60。

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为 ((4⊕1)⊕2)⊕3)=10×2×3+10×3×5+10×5×10=710((4⊕1)⊕2)⊕3)=10×2×3+10×3×5+10×5×10=710。

输入格式

输入的第一行是一个正整数 NN,表示项链上珠子的个数。

第二行是 NN 个用空格隔开的正整数,所有的数均不超过 10001000,第 ii 个数为第 ii 颗珠子的头标记,当 i<Ni<N 时,第 ii 颗珠子的尾标记应该等于第 i+1i+1 颗珠子的头标记,第 NN 颗珠子的尾标记应该等于第 11 颗珠子的头标记。

至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。

输出格式

输出只有一行,是一个正整数 EE,为一个最优聚合顺序所释放的总能量。

数据范围

4≤N≤1004≤N≤100,
1≤E≤2.1×109

题解

题目性质捕捉:

  1. 有n个珠子围成一个环,每次聚合珠子的能量的顺序是:任意选择一个珠子,然后顺时针聚合
  2. 每个珠子都有一个头标记和尾标记,且每两个相邻的挨着的标记是相同的。
  3. 每次聚合的能量等于前一颗珠子的头标记 * 相邻的标记 * 后一颗的尾标记;且聚合之后两颗珠子变成一颗珠子,挨着的标记就没了,新的珠子的头标记等于前一颗的头标记,后标记等于后一颗的后标记。

这也是一个环形区间DP,需要特殊处理的是:聚合的方式很特殊,为了方便计算聚合之和的能量,在末尾加上最后一颗珠子的尾标记,也就是第一颗的头标记(这些珠子是形成一个环)。

并且按照处理环形区间DP问题,追加一份到末尾。且每次计算的区间长度为 n + 1,整个区间长度为 2 * n。

状态计算:也是将每一个区间分为左边和右边,左边最大的能量加上右边最大的能量加上左边和右边区间全部聚合为一个珠子之后再聚合的能量。f[l][k] + f[k][r] + w[l] * w[k] * w[r]

#include<iostream>

using namespace std;

const int N = 210, INF = 1e9;

int n;
int w[N];
int f[N][N];


int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> w[i];
        w[i + n] = w[i];
    }
    
    for(int len = 3; len <= n + 1; len++)
    {
        for(int l = 1; l + len - 1 <= 2 * n; l++)
        {
            int r = l + len - 1;
            f[l][r] = -INF;
            for(int k = l + 1; k < r; k++)
            {
                f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
            }
        }
    }
    
    int res = 0;
    for(int i = 1; i <= n; i++)
        res = max(res, f[i][i + n]);  // 区间长度为n + 1
        
    cout << res << endl;
    return 0;
}

结尾

这是在acwing学习的记录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值