DP动态规划企业级模板分析(数字三角,上升序列,背包,状态机,压缩DP)

前言

经过基础的算法模型讲解和题海战术,将DP动态规划这个重点呢考纲进行细分题类型,此篇是上半章节的DP动态分析提升题。主要包括数字三角形,以及最大上升序列,背包问题,状态机的拆分以及优化状态机的压缩问题。

数字三角形

数字三角形直观来讲就是斜对角的数据DP,从一个点从下从右移动到对角,数据的路径记录或者输出最短。

摘花生

在这里插入图片描述
通过题意我们知道了,整个路径必然是向下或者向右到达右下角的二维路径,并且根据每一个结点可能存在的权值,进行加和,算出来最优价值的路径,在之后的变形中,我们可以约束他的位置,路径,步数,或者多循环线路等。
使用闫式DP分析法
确定其状态表示二维中的最大值,根据两种方向的移动,F[i][j]是由本情况下的F[i-1][j]或者F[i][j-1]移动而来。

#include<iostream>
using namespace std;

const int N = 105;
int a[N][N], f[N][N];
int q, row, col;

int main()
{
    cin >> q;
    while(q--){
        cin >> row >> col;
        for(int i = 1; i <= row; i++){
            for(int j = 1; j <= col; j++){
                cin >> a[i][j];
            }
        }

        // f[i][j]指的是到(i, j)的最大花生数
        for(int i = 1; i <= row; i++){
            for(int j = 1; j <= col; j++){
                f[i][j] = max(f[i-1][j], f[i][j-1]) + a[i][j];
            }
        }

        cout << f[row][col] << endl;
    }

    return 0;
}

最低通行费用

一个商人穿过一个 N×N 的正方形的网格,去参加一个非常重要的商务活动。
他要从网格的左上角进,右下角出。
每穿越中间 1 个小方格,都要花费 1 个单位时间。
商人必须在 (2N−1) 个单位时间穿越出去。
而在经过中间的每个小方格时,都需要缴纳一定的费用。
这个商人期望在规定时间内用最少费用穿越出去。
请问至少需要多少费用?
注意:不能对角穿越各个小方格(即,只能向上下左右四个方向移动且不能离开网格)。

划重点,注意下单位时间,这个也就变相的说明了,卡了他的步数,经过n*n的图形大小说明他还是只能向下或者向右 移动也就是又一个三角形DP,且求路径最小值。
在曼哈顿距离最短路径算法d=x坐标轴差值+y轴坐标差值。
最简单的DP方法

memset(f, 0x3f, sizeof f);
    f[1][1] = w[1][1];

    //dp
    for (int i = 1; i <= n; ++ i)
    {
        for (int j = 1; j <= n; ++ j)
        {
            f[i][j] = min(f[i][j], f[i - 1][j] + w[i][j]);
            f[i][j] = min(f[i][j], f[i][j - 1] + w[i][j]);
        }
    }

记忆化搜索(DFS)

int dp(int x, int y)
{
    if (f[x][y] >= 0) return f[x][y];
    if (x == 1 && y == 1) return w[x][y];
    if (x < 1 || y < 1) return INF;

    int t = INF;
    t = min(t, dp(x - 1, y));
    t = min(t, dp(x, y - 1));
    return f[x][y] = t + w[x][y];
}//上面的代码if的判断使用是为了确定是否有越界的情况。

方格取数

在这里插入图片描述
方格取数因为有两条路径的循环的目的,我们当时会考虑到有最优解,那必然是贪心算法,但是根据条件不能互相影响的条件,贪心会影响,所以不能分开走,还得同时按照DP走
同时走:分别从(1,1)出发走到终点(n, n)
同时走两条路径,令(i1,j1),(i2,j2)分别表示两条路径所经过的点
dp[i1][j1][i2][j2] 表示所有从(1,1)(1,1)走到(i1,j1)(i2,j2)路径的最大值
当i1 == i2 && j1 == j2时代表两条路径出现重复点,则只取一次a[i1][j1]的值
而我们所求中,对结果造成干扰的特殊情况正是‘出现重复点’,我们可以只考虑同时走(二者同时走一步)
优化一下可发现,因为每步同时走 i1 + j1 == i2 + j2 必然成立
可令k = i1 + j1
则dp[i1][j1][i2][j2] 可转化为 dp[k][i1][i2]

for(int i=1;i<=n;i++)
   for(int j=1;j<=n;j++)
    for(int k=1;k<=n;k++)
     for(int l=1;l<=n;l++)
      {int temp;
      if(i==l&&j==k)
            temp=a[i][j];        
     else
         temp=a[i][j]+a[k][l];
        dp[i][j][k][l]=max(dp[i-1][j][l][k-1],dp[i][j][l][k]);
        dp[i][j][k][l]=max(dp[i-1][j][l-1][k],dp[i][j][l][k]);
        dp[i][j][k][l]=max(dp[i][j-1][l-1][k],dp[i][j][l][k]);
        dp[i][j][k][l]=max(dp[i][j-1][l][k-1],dp[i][j][l][k]);
        dp[i][j][k][l]+=temp;
      }
  cout<<dp[n][n][n][n]<<endl;

传纸条

小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题。
一次素质拓展活动中,班上同学安排坐成一个 m 行 n 列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法直接交谈了。
幸运的是,他们可以通过传纸条来进行交流。
纸条要经由许多同学传到对方手里,小渊坐在矩阵的左上角,坐标 (1,1),小轩坐在矩阵的右下角,坐标 (m,n)。
从小渊传到小轩的纸条只可以向下或者向右传递,从小轩传给小渊的纸条只可以向上或者向左传递。 
在活动进行中,小渊希望给小轩传递一张纸条,同时希望小轩给他回复。
班里每个同学都可以帮他们传递,但只会帮他们一次,也就是说如果此人在小渊递给小轩纸条的时候帮忙,那么在小轩递给小渊的时候就不会再帮忙,反之亦然。 
还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低(注意:小渊和小轩的好心程度没有定义,输入时用 0 表示),可以用一个 0∼100 的自然数来表示,数越大表示越好心。
小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径,使得这两条路径上同学的好心程度之和最大。
现在,请你帮助小渊和小轩找到这样的两条路径。
首先, 从右下角回传可以等价为从左上角同时传两次。要想两个路径除了起点和终点之外没有交点,那么肯定有一条路径完全位于另一条的上方。如果存在交点,这种情况其实转换起来很简单,只要把位于红色线段上方的蓝色线段交换颜色就可以了,也就是说当红色处于蓝色的下方的时候,将红色的路径换成从蓝色的那段走是等效的(因为两条路径加起来经过的节点完全没有变)。
代码借用了方格取数的代码,但是思考的逻辑要遵循上面的交点代换。

for(int k=2;k<=n+m;k++){
        for(int i=1;i<k;i++){
            for(int j=1;j<k;j++){
                int &v = f[k][i][j];
                int tmp = mat[i][k-i];
                if(i!=j){
                    tmp += mat[j][k-j];

                }
                v = max(f[k - 1][i - 1][j], v);
                v = max(f[k - 1][i - 1][j - 1], v);
                v = max(f[k - 1][i][j - 1], v);
                v = max(f[k - 1][i][j], v);
                v += tmp;    

            }
        }
    }

最长上升子序列模型

求在一个序列中的单调的子序列,我们之前见过的滑动窗口优化也就是用了最长子序列的单调输出。整体是比较简单的模拟类题解

怪盗基德的滑翔翼

假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。
初始时,怪盗基德可以在任何一幢建筑的顶端。
他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。
因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。
他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。
请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?
求一个高度单调的房子序列,从左侧的单调递增,右侧的单调递减,然后取其中的max跨动子序列

 memset(f_up, 0, sizeof f_up);
        memset(f_dw, 0, sizeof f_dw);
        scanf("%d", &n);
        for (int i = 1; i <= n; ++ i) scanf("%d", &w[i]);

        //最长上升子序列
        //哨兵,不设置的话,需要在循环里额外写一条f[i]=1作为初值
        w[0] = 0;
        for (int i = 1; i <= n; ++ i)
        {
            for (int j = 0; j < i; ++ j)
            {
                if (w[i] > w[j]) f_up[i] = max(f_up[i], f_up[j] + 1);
            }
        }

        //反向最长上升
        w[n + 1] = 0;
        for (int i = n; i; -- i)
        {
            for (int j = n + 1; j > i; -- j)
            {
                if (w[i] > w[j]) f_dw[i] = max(f_dw[i], f_dw[j] + 1);
            }
        }

        int res = 0;
        for (int i = 1; i <= n; ++ i)
        {
            res = max(res, f_up[i]);
            res = max(res, f_dw[i]);
        }
        printf("%d\n", res);

登山

发现山上一共有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。
同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。
队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?

这道题和上面的没啥区别,题目的设计中加了额外条件是不能平行下山,也就是找到下山的时候就意味着是下降子序列。之前还是上升子序列。
所以可以用两个标记。上升0下降1,二维加和即可

for (int i = 1; i <= n; ++ i)
    {
        f[i][0] = f[i][1] = 1;
        for (int k = 1; k < i; ++ k)
        {
            if (a[k] < a[i]) f[i][0] = max(f[i][0], f[k][0] + 1);
            if (a[k] > a[i]) f[i][1] = max(f[i][1], max(f[k][0], f[k][1]) + 1);
        }
    }

    //find result from all final states
    int res = 0;
    for (int i = 1; i <= n; ++ i) res = max(res, max(f[i][0], f[i][1]));
    cout << res << endl;

合唱队形

N 位同学站成一排,音乐老师要请其中的 (N−K) 位同学出列,使得剩下的 K 位同学排成合唱队形。     
合唱队形是指这样的一种队形:设 K 位同学从左到右依次编号为 1,2…,K,他们的身高分别为 T1,T2,…,TK,  则他们的身高满足 T1<…Ti+1>…>TK(1≤i≤K)。     
你的任务是,已知所有 N 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

也就说最少移动使得中间高两边低。
也许会发现和上面的登山有相似的地方,但是你会现在,登山不仅存在特殊要求而且也有路径的覆盖。所以这道题我们采取的分开考虑,然后在i点确定保留的最大值,值得注意的地方是子序列有重复的情况。-1

for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
    for (int i = 1; i <= n; i ++ )
    {
        f[i] = 1;
        for (int j = 1; j < i; j ++ )
            if (h[j] < h[i])
                f[i] = max(f[i], f[j] + 1);
    }

    for (int i = n; i; i -- )
    {
        g[i] = 1;
        for (int j = n; j > i; j -- )
            if (h[j] < h[i])
                g[i] = max(g[i], g[j] + 1);
    }

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[i] + g[i] - 1);

友好城市

每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。
编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。

建桥不交叉尽可能较多的搭建,用图形的方式进行展示
在这里插入图片描述
根据图像展示,我们发现,如果建桥成功而且不交叉的情况,必然是从小到大的排列,也就是上坐标从小到大排列后,下坐标最长上升子序列。
可以用pair以配对的形式存储。然后就是最长子序列的模板

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
typedef pair<int,int> PII;
PII q[N];
int f[N];
int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++)cin>>q[i].first>>q[i].second;
    sort(q,q+n);
    for(int i=0;i<n;i++){
        f[i]=1;
        for(int j=0;j<=i;j++){
            if(q[i].second>q[j].second)f[i]=max(f[i],f[j]+1);
        }
    }
    int res=0;
    for(int i=0;i<n;i++){    
      res=max(res,f[i]);
    }
    cout<<res<<endl;
    return 0;
}

最长上升子序列和

一个数的序列 bi,当 b1<b2<…<bS 的时候,我们称这个序列是上升的。
对于给定的一个序列(a1,a2,…,aN),我们可以得到一些上升的子序列(ai1,ai2,…,aiK),这里1≤i1<i2<…<iK≤N
比如,对于序列(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)。
也就侧面说出,这子序列h和不能用直接路径和,还是直接用上升序列记录路径然后加和。也就是上升子序列+DP和

#include <iostream>
using namespace std;
const int N = 1010;
int a[N], f[N], n;
int main()
{
    cin >> n;
    for(int i = 1; i <= n; i ++) cin >> a[i], f[i] = a[i];
    for(int i = 2; i <= n; i ++)
        for(int j = 1; j < i; j ++)
            if(a[j] < a[i])
                f[i] = max(f[i], f[j] + a[i]);

    int maxn = -1;
    for(int i = 1; i <= n; i ++) maxn = max(maxn, f[i]);
    cout << maxn << endl;
    return 0;
}

最长上升子序列

只是计算上升的序列

#include<bits/stdc++.h>
using namespace std;
const int N = 1e3+5;
int n;
int dp[N], a[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 ++){
        dp[i] = 1;
        for(int j = 1; j < i; j ++)
            if(a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1);
        res = max(res, dp[i]);
    }
    cout << res << endl;

    return 0;
}

拦截导弹

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
某天,雷达捕捉到敌国的导弹来袭。
由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

解决方法有贪心和DP,有机结合可以解决。如果用DP,那就是第一个拦截最多的,剩下的交给下一组。

for(int i = 0; i < n; i ++)
    {
        f[i] = 1;
        g[i] = 1;
        for(int j = 0; j < i; j ++)
        {
            if(a[i] <= a[j]) f[i] = max(f[i], f[j] + 1);
            else g[i] = max(g[i], g[j] + 1);
        }
        ans1 = max(ans1, f[i]);
        ans2 = max(ans2, g[i]);
    }

题目的第二问,对于第i号导弹,要么选择末尾导弹高度最小的拦截系统,要么新创一个拦截系统,用一个数字即每套拦截系统此时所拦截的最后一个导弹高度,来表示该系统。
这样就得到了一个数组,数组最终长度就是所需最少拦截系统数目。

之后介绍一下系统自带的优化算法
在这里插入图片描述
若数组是上升序列
lower同理,返回数组[begin, end)之间第一个大于或等于a的地址,找不到返回end
upper返回数组[begin, end)之间第一个大于a的地址,找不到返回end
其实是向上补齐还是向下补齐,以及提一下最长见的结构体比较cmp手写

bool cmp(node a, node b)
{
    if(a.value1 != b.value1) return a.value1 < b.value1;
    else return a.value2 < b.value2;
}

下面是确定系统的时候使用贪心的算法

for(int i = 0; i < n; i ++)
    {
        f[i] = 1;
        for(int j = 0; j < i; j ++)
        {
            if(a[i] <= a[j]) f[i] = max(f[i], f[j] + 1);
        }
        ans = max(ans, f[i]);
        //数组g的每个元素代表一套导弹拦截系统的拦截序列
        //g[i]表示此时第i套导弹拦截系统所拦截的最后一个导弹的高度
        int p = lower_bound(g, g+cnt, a[i]) - g;
        if(p == cnt) g[cnt ++] = a[i];  //a[i]开创一套新拦截系统    
        else g[p] = a[i];               //a[i]成为第p套拦截系统最后一个导弹高度
    }

若是使用贪心的算法,也就是拦截是up,系统数量是low

int main()
{
    //cnt表示导弹系统数,len表示一个系统最多能拦截的导弹数
    int len = 0, cnt = 0;
    int a;
    while(cin >> a)
    {
        //pos1表示以a结尾的最长不升子序列长度
        int pos1 = upper_bound(f, f+len, a, greater<int>()) - f;
        if(pos1 == len) f[len ++] = a;
        else f[pos1] = a;

        int pos2 = lower_bound(g, g+cnt, a[i]) - g;
        if(pos2 == cnt) g[cnt ++] = a;
        else g[pos2] = a;
    }
    cout << len << endl;
    cout << cnt << endl;
    return 0;
}

导弹防御系统

一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。
例如,一套系统先后拦截了高度为 3 和高度为 4 的两发导弹,那么接下来该系统就只能拦截高度大于 4 的导弹。
给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。

进化 后的题目更加全面,单调序列拦截
up[] 存储当前所有上升子序列的末尾元素
down[] 存储当前所有下降子序列的末尾元素
使用贪心的思想
x 尽可能覆盖一个末尾元素大的序列
因此是一个很强的优化,所以每一步实际上只有两种选择,要么上升,要么下降。
up本身是单调的,所以一方面找到即停止,另一方面可以直接考虑用二分
先用最简单的解法dfs

void dfs(int u, int su, int sd) {
    if (su + sd >= ans) return; // ans不可能再小了

    if (u == n) {
        ans = su + sd;  // su, sd 分别表示 len(up[]), len(down[])
        return;
    }

    // 情况1:将当前数放到上升子序列中
    int k = 0;
    while (k < su && up[k] >= q[u]) k ++;
    int t = up[k];
    up[k] = q[u];
    if (k < su) dfs(u + 1, su, sd);
    else dfs(u + 1, su + 1, sd);
    up[k] = t;

    // 情况2:将当前数放到下降子序列中。
    k = 0;
    while (k < sd && down[k] <= q[u]) k ++;
    t = down[k];
    down[k] = q[u];
    if (k < sd) dfs(u + 1, su, sd);
    else dfs(u + 1, su, sd + 1);
    down[k] = t;
}

迭代加深二分法,大佬的代码,只可远观不可亵玩焉

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

using namespace std;

const int N = 55;

int n;
int up[N], down[N], a[N];

bool dfs(int depth, int u, int su, int sd) {
    if (su + sd > depth) return false;
    if (u == n) return true; // u表示的是枚举每个顶点

    if (!su || up[su] >= a[u]) {
        up[su + 1] = a[u];
        if (dfs(depth, u + 1, su + 1, sd)) return true;
    } else {
        int l = 1, r = su;
        while (l < r) { // 坐标
            int mid = (l + r) >> 1;
            if (up[mid] < a[u]) r = mid;
            else l = mid + 1;
        }
        int t = up[l];
        up[l] = a[u];
        if (dfs(depth, u + 1, su, sd)) return true;
        up[l] = t;
    }

    if (!sd || down[sd] <= a[u]) {
        down[sd + 1] = a[u];
        if (dfs(depth, u + 1, su, sd + 1)) return true;
    } else {
        int l = 1, r = sd;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (down[mid] > a[u]) r = mid;
            else l = mid + 1;
        }
        int t = down[l];
        down[l] = a[u];
        if (dfs(depth, u + 1, su, sd)) return true;
        down[l] = t;
    }

    return false;
}

int main() {
    while (cin >> n, n) {
        for (int i = 0; i < n; i ++) cin >> a[i];

        int depth = 0;

        while (!dfs(depth, 0, 0, 0)) depth ++; 
        cout << depth << endl;
    }
    return 0;
}

最长公共上升子序列

两个数列 A 和 B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。

F[i][j]代表所有a[1–i]和b[i–j]中以b[j]结尾的公共上升子序列的集合,且等于该集合的子序列中长度最大值

首先依据公共子序列中是否包含a[i],将f[i][j]所代表的集合划分成两个不重不漏的子集:

不包含a[i]的子集,最大值是f[i - 1][j];
包含a[i]的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]中是哪个数:
子序列只包含b[j]一个数,长度是1;
子序列的倒数第二个数是b[1]的集合,最大长度是f[i - 1][1] + 1;

子序列的倒数第二个数是b[j - 1]的集合,最大长度是f[i - 1][j - 1] + 1;
如果直接按上述思路实现,需要三重循环:

for (int i = 1; i <= n; i ++ )
{
    for (int j = 1; j <= n; j ++ )
    {
        f[i][j] = f[i - 1][j];
        if (a[i] == b[j])
        {
            int maxv = 1;
            for (int k = 1; k < j; k ++ )
                if (a[i] > b[k])
                    maxv = max(maxv, f[i - 1][k] + 1);
            f[i][j] = max(f[i][j], maxv);
        }
    }
}

然后我们发现每次循环求得的maxv是满足a[i] > b[k]的f[i - 1][k] + 1的前缀最大值。
所以就缩减了循环级

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &b[i]);

    for (int i = 1; i <= n; i ++ )
    {
        int maxv = 1;
        for (int j = 1; j <= n; j ++ )
        {
            f[i][j] = f[i - 1][j];
            if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
            if (a[i] > b[j]) maxv = max(maxv, f[i - 1][j] + 1);
        }
    }

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
    printf("%d\n", res);

    return 0;
}

背包问题

结合之前的背包11讲,这次只讲细节题

机器分配

总公司拥有M台 相同 的高效设备,准备分给下属的N个分公司。
各分公司若获得这些设备,可以为国家提供一定的盈利。盈利与分配的设备数量有关。
问:如何分配这M台设备才能使国家得到的盈利最大?
求出最大盈利值。分配原则:每个公司有权获得任意数目的设备,但总台数不超过设备数M。
从图论的角度思考问题,可以将每一个状态看做一个点,状态的转移看成一个边,把状态的值理解成最短路径长,
在这里插入图片描述
点 f[i][j] 来说,他的 最短路径长 是通过所有到他的 边 更新出来的
更新 最短路 的 规则 因题而已,本题的 更新规则 是 f(i,j)=maxf(i−1,j−vi)+wif(i,j)=maxf(i−1,j−vi)+wi
最终,我们会把从 初始状态(起点)到 目标状态 (终点)的 最短路径长 更新出来
随着这个更新的过程,也就在整个 图 中生成了一颗 最短路径树
该 最短路径树 上 起点 到 终点 的 路径 就是我们要求的 动态规划的状态转移路径
DP的方式就很简单了,主要参透一下dfs深搜

#include <iostream>

using namespace std;

const int N = 20;

int n, m;
int w[N][N];
int f[N][N];
int path[N], cnt;

void dfs(int i, int j)
{
    if (!i) return;
    //寻找当前状态f[i][j]是从上述哪一个f[i-1][k]状态转移过来的
    for (int a = 0; a <= j; ++ a)
    {
        if (f[i - 1][j - a] + w[i][a] == f[i][j])
        {
            path[cnt ++ ] = a;
            dfs(i - 1, j - a);//这个操作和DP中的二维DP是一样的效果,只不过dfs也就是将上一个状态传递下去了
            return;
        }
    }
}
int main()
{
    //input
    cin >> n >> m;
    for (int i = 1; i <= n; ++ i)
        for (int j = 1; j <= m; ++ j)
            cin >> w[i][j];

    //dp
    for (int i = 1; i <= n; ++ i)
        for (int j = 1; j <= m; ++ j)
            for (int k = 0; k <= j; ++ k)
                f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
    cout << f[n][m] << endl;

    //find path
    dfs(n, m);
    for (int i = cnt - 1, id = 1; i >= 0; -- i, ++ id)
        cout << id << " " << path[i] << endl;
    return 0;
}

开心的金明,可是我不开心

今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:
如果要买归类为附件的物品,必须先买该附件所属的主件。
每个主件可以有0个、1个或2个附件。
附件不再有从属于自己的附件。
金明想买的东西很多,肯定会超过妈妈限定的N元。
于是,他把每件物品规定了一个重要度,分为5等:用整数1~5表示,第5等最重要。
他还从因特网上查到了每件物品的价格(都是10元的整数倍)。
他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第j件物品的价格为v[j],重要度为w[j],共选中了k件物品,编号依次为j1,j2,…,jk,则所求的总和为:
v[j1]∗w[j1]+v[j2]∗w[j2]+…+v[jk]∗w[jk](其中*为乘号)
请帮助金明设计一个满足要求的购物单。

本身是开心的金明原题,然后加了附件将背包问题升级了,这道题卡了一上午,反正很emo
也许想使用暴力破解,但是附件的情况多样性,还是很无语的,所以要用到等会要讲的状态压缩,也是在贪心的时候用到过的二进制压缩优化。将附件的选择变成01进制
本题是一道 背包DP 的 经典变种题目:有依赖的背包 DP

根据题设的 拓扑结构 可以观察出每个 物品 的关系构成了 森林

而以往的 背包DP 每个 物品 关系是 任意的(但我们一般视为 线性的)

所以,这题沿用 背包DP 的话,要从原来的 线性DP 改成 树形DP 即可

在 有依赖的背包问题【有依赖背包DP+子物品体积集合划分】 中我们利用了子物品的体积进行 集合划分

时间复杂度是 O(N×V×V)O(N×V×V)
但是对于本题 VV 的数据范围是 3.2×1043.2×104,如果用该种集合 划分方案,毫无疑问会超时

注意到题目中有提到,每个 主件 的 附属品 数量不超过 22 个,且 附物品 不会再有 附属品

因此我们可以采用 分组背包 对本题的状态进行 集合划分

具体思路就是,枚举所有选 子物品 的 组合,每一个 组合 对应一个分组背包中的 物品

这样时间复杂度是 O(N×4×V)

bool not_aff[N];//标记每个分组背包
vector<int> aff[N];//存储每个分组背包内的物品

//dp
void dp(int u, int j)
{
    int siz = aff[u].size();
    //二进制枚举,列举出所有的分组背包内的物品
    for (int st = 0; st < 1 << siz; ++ st)
    {
        int v_sum = v[u], w_sum = w[u];//必须购买主键后,附件才有价值
        for (int i = 0; i < siz; ++ i)
        {
            if (st >> i & 1)
            {
                v_sum += v[aff[u][i]];
                w_sum += w[aff[u][i]];
            }
        }
        //状态转移
        if (v_sum <= j) f[j] = max(f[j], f[j - v_sum] + w_sum);
    }
}
int main()
{
    //input
    cin >> m >> n;
    for (int i = 1; i <= n; ++ i)
    {
        int fa;
        cin >> v[i] >> w[i] >> fa;
        w[i] *= v[i];
        if (fa) aff[fa].push_back(i);
        else not_aff[i] = true;//标记分组背包的物品组
    }
    //dp
    for (int i = 1; i <= n; ++ i)
        if (not_aff[i]) //分组背包
            for (int j = m; j >= 0; -- j)
                dp(i, j);
    //output
    cout << f[m] << endl;
    return 0;
}

能量石(史诗级难度)

在这里插入图片描述
首先一共有n块石头,因为降到最小为0
我们可以视作我们这n块石头全取
那我们暂时拥有所有的能量
接下来我们再减去时间流逝的能量
那么我们就是要将流逝的能量最小化,那么我们要的能量就能最大化
分析相邻两个石头i和i+1
先选i再选i+1得到能量是Ei+Ei+1−Si×li+1Ei+Ei+1−Si×li+1
先选i+1再选i得到的能量是Ei+1+Ei−Si+1×LiEi+1+Ei−Si+1×Li
那么贪心的方式就是先选能量损失小的
那么就是Si∗li+1<Si+1×Li
要注意的时候随时间的变换,整体的价值体系也跟着变换
背包容量不是那么明显,我们去想一下这里把什么当作背包容量不会错过最优解,在处理第i件物品选不选的时候,如果选的话,第i件物品损失的能量就是从起点到第i-1件所花费的所有时间再乘上第i件物品的损失率,所以可以看出所花费的总时间就是“背包容量”,这里背包容量最大就是所有物品都选,把所有物品的时间加在一起

#include <bits/stdc++.h>
using namespace std;

const int N = 10010;

struct store{
    int s, e, l;       
    bool operator< (const store& a){
        return s*(a.l) < l*(a.s);
    }
}stores[N];

int n;
int dp[N];

int main(){
    int T;cin >> T;
    int cnt = 1;

    while(T--){
        int m = 0;
        cin >> n;
        for(int i = 1;i <= n;++i) {
            cin >> stores[i].s >> stores[i].e >> stores[i].l;
            m += stores[i].s;
        }
        sort(stores+1, stores+1+n);

        memset(dp, -0x3f, sizeof dp);
        dp[0] = 0;
        //01 背包
        for(int i = 1;i <= n;++i)
            for(int j = m;j >= stores[i].s;--j)
                dp[j] = max(dp[j], dp[j-stores[i].s] + max(0, stores[i].e - (j-stores[i].s) * stores[i].l));

        int ans = 0;
        for(int j = 1;j <= m;++j) ans = max(ans ,dp[j]);

        cout << "Case #" << cnt++ << ":" << " " << ans << endl;
    }
}

状态机模型

大盗阿福

阿福事先调查得知,只有当他同时洗劫了两家相邻的店铺时,街上的报警系统才会启动,然后警察就会蜂拥而至。
作为一向谨慎作案的大盗,阿福不愿意冒着被警察追捕的风险行窃。
他想知道,在不惊动警察的情况下,他今晚最多可以得到多少现金?

其实也是DP的一种,明白的就是要隔开偷盗,可以使用之前用过的01标记登山状态一样,出现一个冷却期的状态
如果要偷第 i 家店铺,则第 i-1 店铺不能被偷:fi,1=fi−1,0+wi
如果不偷第 i 家店铺,则第 i-1 店铺任意安排:fi,0=max(fi−1,1,fi−1,0)

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

股票买卖

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润,你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易。
同样存在股票保值的问题,又是状态机的标注,难度不是很大,重点是下一道的变形

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

using namespace std;

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

int n, m;
int w[N];
int f[N][M][2];     //f[i][j][0]代表从前i天中选,共进行了j次交易,当前状态为不持有股票的最大收益

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);

    memset(f, -0x3f, sizeof f);                     //要求最大值,先初始化为负无穷
    for (int i = 0; i <= n; i++) f[i][0][0] = 0;    //不管几天,只要没有交易收益就是0  

    for (int i = 1; i <= n; i++) {                  //状态机见上图
        for (int j = 1; j <= m; 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;
    //最后一天,共交易k次,且最后不持有股票的最大值即为结果
    for (int k = 0; k <= m; k ++ ) res = max(res, f[n][k][0]);  

    printf("%d\n", res);

    return 0;
}

股票买卖冷冻期

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
以 线性 的方式 动态规划,考虑第 i 阶段/天 的状态,需要记录的参数有哪些:

第 i 天的 决策状态:
(j=0) 当前没有股票,且不处于冷冻期 (空仓)
(j=1) 当前有股票 (持仓)
(j=2) 当前没有股票,且处于冷冻期 (冷冻期)
在这里插入图片描述
在最后的时候,要保证手里不会有股票或者在冷冻期
普通的状态DP

int f[N][3];

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

    memset(f, -0x3f, sizeof f);
    f[0][0] = 0;
    for (int i = 1; i <= n; ++ i)
    {
        f[i][0] = max(f[i - 1][0], f[i - 1][2]);
        f[i][1] = max(f[i - 1][1], f[i - 1][0] - w[i]);
        f[i][2] = f[i - 1][1] + w[i];
    }
    cout << max(f[n][0], f[n][2]) << endl;
    return 0;
}

滚动数组是一种能够在动态规划中降低空间复杂度的方法.
有时某些二维dp方程可以直接降阶到一维,
简要来说就是通过观察dp方程来判断需要使用哪些数据,可以抛弃哪些数据,
一旦找到关系,就可以用新的数据不断覆盖旧的数据量来减少空间的使用,其实就是压缩空间的想法。思维并不影响。

状态压缩DP

最爱的DP,不是一般的有用二位存。

小国王

在 n×n 的棋盘上放 k 个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。
是否很熟悉的八皇后,那个可以使用DFS,其实也可以DP
由于在第 i 层放置国王的行为,受到 i−1层和 i+1 层以及 i 层的状态影响
那么我们就可以规定从上往下枚举的顺序,这样考虑第 i 层状态时,只需考虑 i−1层的状态即可
于是乎我们可以考虑把层数 i 作为动态规划的 阶段 进行 线性DP
而第 i阶段需要记录的就是前 i 层放置了的国王数量 j,以及在第 i 层的 棋盘状态 k
在这里插入图片描述
在这里插入图片描述
用二进制01标记来控制斜对角和相邻的问题,进行状态转移

#include<bits/stdc++.h>
using namespace std;
typedef long long LL; 
const int N=12;//n+1,省下来额外循环遍历
const int M=1<<10,K=110;//K是我们的国王数量,二进制的10个行标记位置0,1
int n,m;//这里用m来表示国王数量,因为我习惯用n来表示一个数然后m来表示另外一个值
vector<int> state; //state 用来表示所有的合法的状态
vector<int> head[M];//这个是每一状态所有可以转移到的其他状态
int cnt[M];//状态标记转化到实体数里。
LL f[N][K][M];
bool check(int state){//state状态的审核
    for(int i=0;i<n;i++)
     if((state >> i & 1)&&(state >> i + 1 & 1))return false;//如果存在连续两个1的话就不合法
    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;
    for(int i=0;i<1<<n;i++)//同行合法状态预处理
       if(check(i))
    /*check函数是检查一下我们当前状态是不是合法,也就是检查一下我们这个状态里面是不是存在连续的两个1,
    如果不存在的话就表示它是合法的*/
       {
           state.push_back(i);
           cnt[i]=count(i);//cnt的话存的是这个i里面 1 的个数是多少
       }

    //然后我们来看一下这个不同状态之间的一个这个边的关系i和i-1的关系
    for(int i = 0;i< state.size();i ++ )
      for(int j=0;j<state.size();j++){
          //用a来表示第一个状态,用b来表示第二个状态
          int a=state[i],b=state[j];
          //这里是建立一个不同状态之间的转移关系
          //先预处理一下哪些状态和哪些状态之间可以转移
          //首先转移的话是要满足两个条件对吧
          //一个是这个 a 和 b 的交集必须要是空集,它必须是空集才可以,否则的话同一列的两个国王会攻击到
          //并且的话它们的这个这个并集的话也是需要去满足我们不能包含两个相邻的1的
          if((a&b)==0&&check(a|b))
             // head[a].push_back(b);
             //然后只要这个 b 是合法的,那么我们就把 b 添加到 a 可以转移到的状态集合里面去
             head[i].push_back(j);
      }//完成了数据处理,最后进入了DP环节,注意一下n+1的意义
      f[0][0][0]=1;//初始状态f[0][0][0]=1
       for(int i=1;i<=n+1;i++)
         for(int j=0;j<=m;j++)//m表示的是国王数量
           for(int a=0;a<state.size();a++)//然后我们来枚举一下所有的状态,a表示第i行的状态
             for(int b : head[a])//然后来枚举所有a能到的状态
             {
                 int c=cnt[state[a]];
                  if(j>=c)//因为我们这个表示我们当前这行摆放的国王数量一定要小于等于我们整个的上限,如果数说满足要求的话,那么我们就可以转移了{
                     f[i][j][a]+=f[i-1][j-c][b];
                     //转移的话就是f[i][j][a]+=f[i-1][j-c][b],然后从b转移过来
                 }
             }
    cout<<f[n+1][m][0]<<endl;
    /*就是我们假设存在第n+1行,但是第n+1行完全没有摆,
    那这种情况里面的所有方案其实就是等于这个这个我们只有n行的一个所有方案*/
    //所以可以少一层循环
    /*这里的话就是为什么我们一开始N要从12开始,对吧,首先我们要用到11这个下标对吧,
    那其实11这个下标是需要开长度是12才可以*/
    return 0;
}

玉米田

农夫约翰的土地由 M×N 个小方格组成,现在他要在土地里种植玉米。
非常遗憾,部分土地是不育的,无法种植。
而且,相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。
现在给定土地的大小,请你求出共有多少种种植方法。
土地上什么都不种也算一种方法。
其实当很典型的小国王解决之后,这个玉米田就变简单了,结合上一道题设计的思路,这个反而更像是检验题,而且变得简单了,只是需要考虑四向而已

#include <bits/stdc++.h>
using namespace std;
const int N=13,Mod=100000000;
vector<int> state,head[1<<N];
int n,m,x,f[14][1<<N],g[N];
inline bool check(int x)//快速判断有没有相邻的1
{
    return !(x&x>>1);
}
inline void init()
{
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++)
        for (int j=1; j<=m; j++)
        {
            scanf("%d",&x);
            g[i]+=(!x<<(j-1));//荒废土地是0,我们在这里转换为1
        }
    for(int i=0; i<(1<<m); i++)
        if (check(i))//这个状态不存在种植左右相邻的玉米
            state.push_back(i);
    for(int i=0; i<state.size(); i++)
        for(int j=0; j<state.size(); j++)
            if (!(state[i] & state[j]))//i对应的状态和j对应的状态没有在同一列种植玉米
                head[i].push_back(j);
    f[0][0]=1;
    for(int i=1; i<=n+1; i++)
        for(int a=0; a<state.size(); a++)
        {
            if (state[a] & g[i])//在第i行,状态a是否满足在荒废土地没有种植玉米
                continue;
            for(int b=0; b<head[a].size(); b++)//从上一行b对应的状态,转到本行a对应的状态
                f[i][a]=(f[i][a]+f[i-1][head[a][b]])%Mod;
        }
    printf("%d\n",f[n+1][0]);//表示第n+1行什么都没种植的状态,其实就是累加f[n][S]
}

二营长的意大利炮

司令部的将军们打算在 N×M 的网格地图上部署他们的炮兵部队。
一个 N×M 的地图由 N 行 M 列组成,地图的每一格可能是山地(用 H 表示),也可能是平原(用 P 表示),如下图。
在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
在这里插入图片描述
故我们可以把 行 作为动态规划的阶段,然后对于第 i 行的摆放状态进行 状态压缩
这就变成了一道标准的 线性状压DP
但是本题不同于 小国王 和 玉米田,这两题中棋子的 攻击距离 只有1
因此在这两题里,我们只需压缩存储 当前层的状态 ,然后枚举 合法的上个阶段 的状态进行 转移 即可
但是本题的棋子攻击范围是 22,我们只压缩当前层一层状态后进行转移,是不能保证该次转移是 合法的
即不能排除第 i−2i−2 层摆放的棋子可以攻击到第 ii 层棋子的 不合法 情况
而解决该问题的手段就是:压缩存储两层的信息,然后枚举合法的第 i−2i−2 层状态进行转移即可

这道题就是玉米田距离+1版

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

using namespace std;

const int N = 110,M = 1 << 10;
int f[2][M][M];
vector<int>state;
vector<int>pre[M];
int n,m;
int g[N];
int cnt[M];

bool check(int state)
{
    for(int i = 0 ; i < m ; i ++)
        if((state >> i & 1)&&((state >> i + 1 & 1)||(state >> i + 2 & 1)))return false;

    return true;    
}

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

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

    for(int i = 1 ; i <= n ; i ++)  
        for(int j = 0 ; j < m ; j ++)
            {
                char c;
                cin>>c;
                if(c=='H')g[i] += 1 << j;
            }

    for(int i = 0 ; i < 1 << m ; i ++)
        if(check(i))
        {
            state.push_back(i);
            cnt[i] = count(i);
        }

    //预处理合法状态能转移的状态    
    for (auto state_st : state)
        for (auto pre_st : state)
            if (!(state_st & pre_st))
                pre[state_st].push_back(pre_st);//能从pre_st到state_st


    for(int i = 1 ; i <= n + 2 ; i ++)
        for (auto i_st : state)//枚举当前层 
            if (!(g[i] & i_st))//第i层合法
                for (auto j_st : pre[i_st])//枚举上一层
                    if (!(g[i - 1] & j_st))//第i - 1层合法
                        for (auto u_st : pre[j_st])//枚举上二层
                            if (!(i_st & u_st))//判断当前行与前两行冲突
                                f[i & 1][j_st][i_st] = max(f[i & 1][j_st][i_st] , f[i - 1 & 1][u_st][j_st] + cnt[i_st]);

    cout<<f[n + 2 & 1][0][0]<<endl;

    return 0;
}

图论下的状态DP

宝藏

参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋,也给出了这 n 个宝藏屋之间可供开发的 m 条道路和它们的长度。 
小明决心亲自前往挖掘所有宝藏屋中的宝藏。
但是,每个宝藏屋距离地面都很远,也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路则相对容易很多。
小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。 
在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。
已经开凿出的道路可以任意通行不消耗代价。
每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路所能到达的宝藏屋的宝藏。
另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏屋之间的道路无需再开发。
新开发一条道路的代价是:  
这条道路的长度 × 从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋)。 
请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代价最小,并输出这个最小值。

翻译成能看懂的

给定一个 nn 个点,mm 条边且连通的 无向图
初始时,无向图中没有边,每个 点 都是 互不连通 的
我们初始可以选择任意一个点作为 起点,且该选择 起点 的操作 费用 是 00
然后开始维护这个包含起点的 连通块
每次我们选择一个与 连通块内的点 相连的一条边,使得该边的另一个点(本不在连通块内的点)加入连通块
该次选边操作的费用是:边权×该点到起点的简单路径中经过的点数边权×该点到起点的简单路径中经过的点数
最终我们的目标是,使得 所有点都加入当前的连通块内
求解一个 方案 ,在达成目标的情况下,费用最小,输出方案的费用

状态压缩DP,下文中i是一个 n 位二进制数,表示每个点是否存在。
状态f[i][j]表示:
集合:所有包含i中所有点,且树的高度等于j的生成树
属性:最小花费
状态计算:枚举i的所有非全集子集S作为前j - 1层的点,剩余点作为第j层的点。
核心: 求出第j层的所有点到S的最短边,将这些边权和乘以j,直接加到f[S][j - 1]上,即可求出f[i][j]。
证明:
将这样求出的结果记为f’[i][j]
f[i][j]中花费最小的生成树一定可以被枚举到,因此f[i][j] >= f’[i][j];
如果第j层中用到的某条边(a, b)应该在比j小的层,假设a是S中的点,b是第j层的点,则在枚举S + {b}时会得到更小的花费,即这种方式枚举到的所有花费均大于等于某个合法生成树的花费,因此f[i][j] <= f’[i][j]
所以有 f[i][j] = f’[i][j]。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 14,M = 1 << 12,K = 1010;
const int INF = 0x3f3f3f3f;
int f[M][N];//f[i][j]表示在状态i、树(y总所讲的树)高度为j时的最小花费
int g[M];//g[i]表示状态i中所有的点及这些点所能一步达到的所有点,也用二进制表示。
int dist[N][N];//dist[i][j]表示i号点和j号点之间边的长度,若之间没有边,则长度定义为为INF(正无穷)
int n,m;
int main(){
    cin >> n >> m;
    memset(dist,0x3f,sizeof dist);//初始化dist数组为正无穷
    for(int i = 0;i < n;i ++) dist[i][i] = 0;
    //一个点到它自身的距离一定是0,这为g数组g[i]可以表示i状态本身含有的点作下铺垫
    for(int i = 0;i < m;i ++){
        int a,b,c;
        cin >> a >> b >> c;
        a --,b --;
        //因为我们要用状态压缩,故所有点从0开始比较好
        //若从1开始,第一个点就是2的1次方,即10,明显不好统计
        dist[a][b] = dist[b][a] = min(dist[a][b],c);//防止重边
    }
    //这里初始化g数组,方便状态转移计算
    for(int i = 0;i < 1 << n;i ++){//枚举每个状态,例:当i为1010时,表示此状态有第2、4个宝藏点
        for(int j = 0;j < n;j ++)//枚举状态i中的每一位
            if(i >> j & 1){//判定状态i中第j位是否为1,即判定状态i中是否存在j这个宝藏点
                //下面三行用来判断j这个点能否一步到达的所有宝藏点
                for(int k = 0;k < n;k ++)//枚举每一个宝藏点
                    if(dist[j][k] != INF)//判断宝藏点j能否一步到达宝藏点k
                        g[i] |= 1 << k;
                        /*
                        如果能到达,由于宝藏点j包含于状态i中,故状态i能一步到达宝藏点k,g[i]中即可包含k点
                        要注意的是,当j和k相等时,dist[j][k]也满足条件,故g[i]也包含状态i中的所有点
                        */
            }
    }
    memset(f,0x3f,sizeof f);//初始化f数组为正无穷,方便统计最小值
    //"赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道",下面这行表示这个意思
    for(int i = 0;i < n;i ++) f[1 << i][0] = 0;
    //状态转移
    for(int i = 0;i < 1 << n;i ++)//枚举每个状态
        for(int j = (i - 1) & i;j;j = (j - 1) & i){//枚举i的每一个子集
            if((g[j] & i) == i){//g[j]表示j能一步到达的状态,若此状态包含状态i的所有点,则j能一步到达i
                int remain = i ^ j;
                //因为j是i的子集,remain表示i中j的补集,即状态j到达状态i过程中新增的宝藏点的状态

                //下面是为j -> i新增的宝藏点找到最小边的操作,同时统计最小花费
                int cost = 0;//cost表示在状态j到达状态i过程中用到的最小花费
                for(int k = 0;k < n;k ++){//枚举remain状态每一位
                    if(remain >> k & 1){//找出remain状态中的宝藏点
                        int t = INF;//t表示remain中每一位(即新增的宝藏点)到达j中某一点的最小花费
                        for(int u = 0;u < n;u ++)//枚举j状态的每一位
                            if(j >> u & 1)//找出j状态中的宝藏点
                                t = min(t,dist[k][u]);
                        cost += t;
                    }
                }
                for(int k = 1;k < n;k ++)//枚举树的高度,因为这种状态可能出现在任何一层
                    f[i][k] = min(f[i][k],f[j][k - 1] + cost * k);//状态转移
            }           
        }

    //下面就是枚举答案最小值了
    int ans = INF;
    //最小值可能出现在任意一层,比如这道题的样例,从1号点开始挖,途径1-2,1-4,4-3,最大层是2层
    for(int i = 0;i < n;i ++) ans = min(ans,f[(1 << n) - 1][i]);//(1 << n) - 1表示全选状态,即111111...
    cout << ans;
    return 0;
}

结尾

作为DP大章的上半部分,难度适中,题海里做的总结,如是而已,宝藏和小国王题目还是比较核心的压缩
然后今天的彩蛋是
“可能我们的世界很单纯,单纯的用一次表白就能让它焕然一新。”

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

磊哥哥讲算法

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值