【算法竞赛备赛】DP基础—用最短的代码解最复杂的问题

图片无法加载,请点击此处

前言

动态规划,DP(Dynamic Programming),运筹学的一个分支,同时也是求解决策过程最优化问题的过程。它和贪心一样,不是具体的某一种算法,而是一种思想

DP的学习和使用,非常地具有挑战性,会使用DP,经常可以写出令人拍案叫绝的代码。所以它也是算法竞赛,数学建模等中的一个热点问题

DP优化是对方程进行等价变换

背包问题

状态表示与计算

f ( i , j ) f(i,j) f(i,j)存的是集合中所有选法的属性,是一个

集合划分

0-1背包

每个物品只能选一次,即放入/不放入背包,使利益最大化

01背包问题(状态转移方程讲解)深蓝

状态转移方程:

f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w ) f[i,j] = max(f[i-1,j],f[i-1,j-v]+w) f[i,j]=max(f[i1,j],f[i1,jv]+w)

二维

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];

int main() {
    scanf("%d%d", &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++) {
            // 当前背包容量小于物品重量
            if (j < v[i])   f[i][j] = f[i-1][j]; // 表示不选
            else f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]); // 表示选上
        }
    }
    cout << f[n][m] << endl;
    
    return 0;
}

一维

关于内层循环要为何要逆序遍历?

如果顺序遍历,那么数组中来自上一轮的状态,会因顺序原因被污染。导致出现需要上一轮的状态时,却已经被覆盖了的情况。

换言之,如果j是顺序递增的话,则相当于f[i][j]变得是由f[i][j-v[i]]推出来的,

而不是由原来的f[i-1][j-v[i]]推的。与我们的状态转移方程不符。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];


int main() {
    scanf("%d%d", &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;
}

AcWing.0-1背包

完全背包

物品无限量,同一个物品可以重复放入背包中

O(n^3),朴素做法

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

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 = 0; j <= m; j++) // 重量
            for (int k = 0; k * v[i] <= j; 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^2)

对k进行优化,三维降二维,状态转移方程优化推导如下:

f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 v ] + 2 w , . . . ) f[i,j] = max(f[i-1,j],f[i-1,j-v]+w,f[i-1,j-2v]+2w,...) f[i,j]=max(f[i1,j],f[i1,jv]+w,f[i1,j2v]+2w,...)

因为i-1个物品的最大值已经在上一轮循环确定了下来,所以我们只需讨论第i个物品应该选多少个

j替换为j-v

f [ i , j − v ] = m a x ( f [ i − 1 , j − v ] , f [ i − 1 , j − 2 v ] + w , f [ i − 1 , j − 3 v ] + 2 w , . . . ) f[i,j-v] = max(f[i-1,j-v],f[i-1,j-2v]+w,f[i-1,j-3v]+2w,...) f[i,jv]=max(f[i1,jv],f[i1,j2v]+w,f[i1,j3v]+2w,...)

f [ i , j − v ] + w = m a x ( f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 v ] + 2 w , f [ i − 1 , j − 3 v ] + 3 w , . . . ) f[i,j-v]+w = max(f[i-1,j-v]+w,f[i-1,j-2v]+2w,f[i-1,j-3v]+3w,...) f[i,jv]+w=max(f[i1,jv]+w,f[i1,j2v]+2w,f[i1,j3v]+3w,...)

将上述式子代入原方程,进行等价替换,得到状态转移方程:

f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i , j − v ] + w ) f[i,j]=max(f[i-1,j],f[i,j-v]+w) f[i,j]=max(f[i1,j],f[i,jv]+w)

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

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

降维

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

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
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;
}

同时处理输入和状态转移

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

using namespace std;

const int N = 1010;

int n, m;
int f[N];

int main() {
    cin >> n >> m;
    
    for (int i = 1; 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;
}

多重背包

每个物品的数量不同

朴素作法

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

using namespace std;

const int N = 110;

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

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

二进制优化

将一个物品的个数,表示成一段从1开始的二进制数

例如,200:1、2、4、8、16、32、64、73

为什么不选128呢?因为如果加128,可以表示的数就达到了255,超出了200。到64时,表示的数范围为127,补上一个73,就能够表示从1-200之间的任何一个数

  • 同时留意使用二进制优化时,需要开辟的数组空间为物品种数xlog2(某种物品的数量,向上取整)。因为我们在把物品数量拆成二进制表示时,要考虑到要用多少个数,才能表示出物品数量,得出上述结果
#include <iostream>
#include <algorithm>

using namespace std;

// 当有1000种物品,每种物品的数量为2000,得25000
const int N = 25000; // 留意开辟空间
int f[N];
int v[N], w[N];
int n, 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] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if (s > 0) {
            cnt++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }
    n = cnt;
    
    // 0-1背包
    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;
}

分组背包

每一组中选取一样物品加入背包

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

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

线性DP

数字三角形

DP里的Hello World!

数字三角形DP
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, INF = 1e9;

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

int main() {
    cin >> n;
    
    for (int i = 1; i <= n; i++) 
        for (int j = 1; j <= i; j++) 
            scanf("%d", &a[i][j]);
    
    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];
    
    for (int i = 2; i <= n; i++) // 若从1开始,结果为负无穷,由前状态转移而来
        for (int j = 1; j <= i; j++) // 从2开始才有前状态
            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]);
        
    cout << res << endl;
    
    return 0;
}

AcWing.数字三角形

最长上升子序列

到这里为止,学习动态规划的感想是:根据题目,找到其中的一个状态,分析它是由前一个状态怎么转换得来

比方说到下图中的6这个位置上,那么要求以6结尾的最长递增子序列要通过遍历它的前六个数,判断与它们的大小关系,再加1,取最大值。而前6个数的最长又是怎么求出来的?就是根据前5个数,以此类推

image-20220220115331498 最长上升子序列DP
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int a[N], f[N], pre[N];
int n;

void PrintPath(int v) {
    if (pre[v] == 0) {
        printf("%d", a[v]);
        return;
    }
    PrintPath(pre[v]);
    printf(" %d", a[v]);
}

int main() {
    scanf("%d", &n);

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

    for (int i = 1; i <= n; i ++ ) {
        f[i] = 1; // 初始化,只有1个数以第i个数结尾
        for (int j = 1; j < i; j++) {
            if (a[j] < a[i]) {
                if (f[i] < f[j] + 1) {
                    f[i] = f[j] + 1;
                    pre[i] = j; // 记录前一个点的下标,用于还原路径
                }
            }
        }
    }

    int res = -1e9, t;
    for (int i = 1; i <= n; i ++ ) {
        if (f[i] > res) {
            res = f[i];
            t = i; // 记录最长路径的最后一个下标
        }
    }
    cout << res << endl;
    PrintPath(t);

    return 0;
}

AcWing.最长上升子序列

最长公共子序列

  • 状态表示 f [ i , j ] f[i,j] f[i,j]s1的前i个字符和s2的前j个字符的最长公共子序列
  • 状态计算: s 1 [ i ] = s 2 [ j ] s_1[i] = s_2[j] s1[i]=s2[j] s 1 [ i ] ! = s 2 [ j ] s_1[i] != s_2[j] s1[i]!=s2[j]

当两个字符相等时,容易理解,由 f [ i − 1 ] [ j − 1 ] + 1 f[i-1][j-1] + 1 f[i1][j1]+1转移而来

而当两个字符不等时,两个字符中一定有一个可以抛弃 f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] f[i-1][j], f[i][j-1] f[i1][j],f[i][j1]

外一个可能存在于最长序列,也有可能不存在,取最大值避免了复杂的讨论情况

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

using namespace std;

const int N = 1010;

int n, m;
char s1[N], s2[N];
int f[N][N];

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

AcWing.最长公共子序列

区间DP

  • 状态表示 f [ i , j ] f[i,j] f[i,j]:从第i堆石子到第j堆石子合并所需的代价
  • 状态计算:将区间划分成 [ i , k ] [i,k] [i,k] [ k + 1 , j ] [k+1,j] [k+1,j] k = i , i + 1 , . . . , j − 1 k = i, i+1, ..., j-1 k=i,i+1,...,j1

通过枚举区间长度,找出代价的最小值

状态转移: f [ l ] [ k ] + f [ k + 1 ] [ r ] + s [ r ] − s [ l − 1 ] f[l][k] + f[k+1][r] + s[r] - s[l-1] f[l][k]+f[k+1][r]+s[r]s[l1]

最后一次转移加上它的代价

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 310;
int n;
int f[N][N];
int s[N];

int main() {
    scanf("%d", &n);
    
    for (int i = 1; i <= n; i ++ ) scanf("%d", &s[i]);
    
    // 前缀和,确定任一区间的权重
    for (int i = 1; i <= n; i ++ ) s[i] += s[i-1];
    
    // len=1的时候无须合并,所以从2开始
    for (int len = 2; len <= n; len++)
        // 枚举所有长度为len的情况
        for (int i = 1; i + len - 1 <= n; i++) {
            int l = i, r = i + len -1;
            f[l][r] = 1e8;
            // 在区间范围内进行划分
            for (int k = l; k <= r; k++)
                f[l][r] = min(f[l][r], f[l][k] + f[k+1][r] + s[r] - s[l-1]);
        }
    
    cout << f[1][n] << endl;
    
    return 0;
}

AcWing.石子合并

计数类DP

状态表示中属性为count的DP问题

整数划分

一个正整数n可以表示 成若干个正整数之和,形如: n = n 1 + n 2 + . . . + n k n = n_1 + n_2 + ... + n_k n=n1+n2+...+nk,其中 n 1 ≥ n 2 ≥ . . . ≥ n k , k ≥ 1 n_1 \ge n_2 \ge ... \ge n_k, k \ge 1 n1n2...nk,k1

我们将这样的一种表示 称为正整数n的一种划分。

现在给定一个正整数n,请你求出n共有多少种不同的划分方法。

输入格式

共一行,包含一个整数n。

输出格式

共一行,包含一个整数,表示总划分数量。

由于答案可能 很大,输出结果请对 1 0 9 + 7 10^9+7 109+7取模。

数据范围

1 ≤ n ≤ 1000 1 \le n \le 1000 1n1000

转化为完全背包问题

  • 状态表示 f [ i , j ] f[i,j] f[i,j]:从1-i当中选,使体积恰好是j数量
  • 状态计算:针对第i个数时,有不选、选i、选2i、……、选si这些情况,容易将其转化为完全背包问题

选几个i,就要在之前的状态中提前预留出位置,因此减去

f [ i , j ] = f [ i − 1 , j ] + f [ i − 1 , j − i ] + f [ i − 1 , j − 2 i ] + . . . + f [ i − 1 , j − s i ] f[i,j] = f[i-1,j]+f[i-1,j-i]+f[i-1,j-2i]+...+f[i-1,j-si] f[i,j]=f[i1,j]+f[i1,ji]+f[i1,j2i]+...+f[i1,jsi]

j等价替换为j-i

f [ i , j − i ] = f [ i − 1 , j − i ] + f [ i − 1 , j − 2 i ] + f [ i − 1 , j − 3 i ] + . . . + f [ i − 1 , j − s i ] f[i,j-i] = f[i-1,j-i]+f[i-1,j-2i]+f[i-1,j-3i]+...+f[i-1,j-si] f[i,ji]=f[i1,ji]+f[i1,j2i]+f[i1,j3i]+...+f[i1,jsi]

代入原式,得

f [ i , j ] = f [ i − 1 , j ] + f [ i , j − i ] f[i,j]=f[i-1,j]+f[i,j-i] f[i,j]=f[i1,j]+f[i,ji]

#include <iostream>

using namespace std;

const int N = 1010, mod = 1e9 + 7;
int n;
int f[N];

int main() {
    cin >> n;
    
    f[0] = 1; // 一个数都不选,总和为0时的方案
    for (int i = 1; i <= n; i ++ )
        for (int j = i; j <= n; j++)
            f[j] = (f[j] + f[j-i]) % mod;
    
    cout << f[n] << endl;
    
    return 0;
}

计数类DP

  • 状态表示 f [ i , j ] f[i,j] f[i,j]:所有数的总和为i,并且表示成j个数的和的个数
  • 状态计算:将集合划分成两种情况:j个数最小值为1最小值大于1
    • 最小值为1: f [ i − 1 , j − 1 ] f[i-1,j-1] f[i1,j1]
    • 最小值大于1: f [ i − j , j ] f[i-j,j] f[ij,j]每个值都减去1,一共减j,方案数不变

状态转移方程为

f [ i , j ] = f [ i − 1 , j − 1 ] + f [ i − j , j ] f[i,j]=f[i-1,j-1]+f[i-j,j] f[i,j]=f[i1,j1]+f[ij,j]

最后的结果需要对式求和 f [ n ] [ i ] , i = 1 , 2 , . . . , n f[n][i],i=1, 2,...,n f[n][i],i=1,2,...,n

#include <iostream>

using namespace std;

const int N = 1010, mod = 1e9 + 7;
int n;
int f[N][N];

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

数位统计DP

计数问题

给定两个整数 a a a b b b,求 a a a b b b 之间的所有数字中 0 ∼ 9 0∼9 09 的出现次数。

  • 解法一:暴力枚举

  • 解法二:枚举每一位x出现的次数

举例如下

a b c d e f g abcdefg abcdefg

x在第4位(d)上出现的次数

  1. 000 ∼ a b c − 1 000∼abc-1 000abc1,出现 a b c ∗ 1000 abc*1000 abc1000次,1000为10^efg的长度

    判断的数在最高位时,不用考虑情况1

    这里需要讨论一种特殊情况,若判断的x是0出现的次数,则要从001开始,意味着 ( a b c − 1 ) ∗ 1000 (abc-1)*1000 (abc1)1000

  2. a b c abc abc

    1. d < x d<x d<x,出现0次
    2. d = x d=x d=x,出现 e f g + 1 efg+1 efg+1次(000)
    3. d > x d>x d>x,出现1000次(efg:000-999)

另外一种特殊情况是,0不可能在第一位出现,所有求0出现的次数时,应该从数的第二位开始

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

using namespace std;

int a, b;

// 求10^x
int power10(int x) {
    int res = 1;
    while (x--) {
        res *= 10;
    }
    
    return res;
}

// 将num中l到r表示的数提取成一个数
int get(vector<int> num, int l, int r){
    int res = 0;
    
    for (int i = l; i >= r; i--) {
        res = res * 10 + num[i];
    }
    
    return res;
}

// 数位DP
int count(int n, int x) {
    // 1到0中不含有任何数
    if (!n) return 0;
    
    vector<int> num;
    
    // 将n倒序存放
    while (n) {
        num.push_back(n % 10);
        n /= 10;
    }
    
    // 获取n的位数,用于遍历
    n = num.size();
    
    int res = 0;
    // 遍历x在n的每一位上出现的次数,并作和
    // 若x为0,则x必不出现在n的最高位,直接从第二位开始计算
    for (int i = n - 1 - !x; i >= 0; i--) {
        // 情况一
        if (i < n - 1) {
            res += get(num, n-1, i+1) * power10(i);
            if (!x) res -= power10(i);
        }
        // 情况二
        if (num[i] == x) res += get(num, i-1, 0) + 1;
        else if (num[i] > x) res += power10(i);
        
    }
    
    return res;
}

// 暴力解法
int force_count(int n, int x) {
    int res = 0;
    for (int i = 1; i <= n; i++)
        for (int j = i; j; j /= 10)
            if (j % 10 == x) res++;
    
    return res;
}


int main() {
    
    while (cin >> a >> b, a || b) {
        if (a > b) swap(a, b);
        for(int i = 0; i < 10; i++) {
            // 以前缀和的方式,求ab区间i出现的次数
            cout << count(b, i) - count(a-1, i) << ' ';
        }
        cout << endl;
    }
    
    return 0;
}

AcWing 338. 计数问题

状态压缩DP

所谓的状态压缩DP,就是用二进制数保存状态。

通过几道题,得出状态压缩问题有点类似蒙特卡罗。状态压缩DP通过枚举二进制来表示每一状态,和蒙特卡罗中枚举随机数来得到最后的答案非常的相似。

状态压缩位数 N ≤ 20 N \le 20 N20,约等于数据量 1 0 6 10^6 106

蒙德里安的猜想

  • 状态表示 f [ i , j ] f[i,j] f[i,j]f[i][j]表示已经将前 i -1 列摆好,且从第i−1列,伸出到第i列的状态是j的所有方案数。且j是一个由二进制表示出来的数,如10010:第1列和第4列有砖
  • 状态计算: f [ i , j ] + = f [ i − 1 , k ] f[i,j]+=f[i-1,k] f[i,j]+=f[i1,k]

状态转移需要满足两个条件:

  1. 不与前一个状态冲突(j&k)==0
  2. 此状态中0的个数(j|k)为偶
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 12, M = 1 << N;
bool st[M];
int n, m;
long long f[N][M];

int main() {
    while (cin >> n >> m, n || m) {
        // 枚举所有奇数个0的状态,并将其设置为false
        for (int i = 0; i < 1 << n; i++) {
            st[i] = true;
            int cnt = 0;
            
            for (int j = 0; j < n; j++) {
                if (i >> j & 1) {
                    if (cnt & 1) st[i] = false;
                    cnt = 0;
                } else cnt++;
            }
            if (cnt & 1) st[i] = false;
        }
        
        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++)
                    // 若不与前一个状态冲突且此状态中0的个数为偶 
                    if ((j & k) == 0 && st[j|k]) f[i][j] += f[i-1][k];
            
        cout << f[m][0] << endl;
    }
    
    return 0;
}

291.蒙德里安的梦想

最短Hamilton路径

  • 状态表示 f [ i , j ] f[i,j] f[i,j]:走过的点用二进制记录在i中,从0到达j最短路径
  • 状态计算: f [ i , j ] = f [ i − j , k ] + w [ k . j ] f[i,j]=f[i-{j}, k] + w[k.j] f[i,j]=f[ij,k]+w[k.j]

二进制数100101意思是经过了第1、3、6点(倒着看)

​ 0->…->k->j

k走到j点需要满足条件:k可以到达j。

不过在DP中,是先枚举出了状态,再去判断这个状态是否满足这个条件,如果满足再进行相应的计算

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

using namespace std;

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

int n;
int w[N][N];
int f[M][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);
    
    // 初始化,当在第一个点时,路径为0
    f[1][0] = 0;
    
    for (int i = 0; i < 1 << n; i++) // 枚举可能的所有到达情况
        for (int j = 0; j < n; j++) // 枚举,当前到达了j点
            if (i >> j & 1) // 表示从0可以到达j点
                for (int k = 0; k < n; k++) // 枚举点k(j的前一个点)
                    if ((i - (1 << j)) >> k & 1) // 先减掉目的点j后,需要先能到达k点
                        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;
}

91.最短Hamilton路径

树形DP

没有上司的舞会

一道非常经典的树形DP问题

  • 状态表示 f [ i ] [ 0 ] , f [ i ] [ 1 ] f[i][0],f[i][1] f[i][0],f[i][1]:0表示当前i结点不选,1表示选
  • 状态计算: f [ i ] [ 0 ] + = m a x ( f [ j ] [ 0 ] , f [ j ] [ 1 ] ) f[i][0] += max(f[j][0], f[j][1]) f[i][0]+=max(f[j][0],f[j][1]) f [ i ] [ 1 ] + = f [ j ] [ 0 ] f[i][1] += f[j][0] f[i][1]+=f[j][0]

如果当前结点未选的话,代表未去舞会,那么他的下属就有两种选择,去或不去,选取最大的

如果当前结点被选的话,他的下属只有不去这一种选法

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

using namespace std;

const int N = 6010;
int n;
int h[N], e[N], ne[N], idx;
int happy[N];
bool st[N];
int f[N][2];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u) {
    f[u][1] = happy[u];
    
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        dfs(j);
        
        // 上司不去舞会
        f[u][0] += max(f[j][0], f[j][1]);
        // 上司去舞会
        f[u][1] += f[j][0];
    }
}


int main() {
    cin >> n;
    
    for (int i = 1; i <= n; i++) cin >> happy[i];
    
    memset(h, -1, sizeof h);
    for (int i = 0; i < n-1; i++) {
        int a, b;
        cin >> a >> b;
        st[a] = true;
        add(b, a);
    }
    
    int root = 1;
    while(st[root]) root++;
    
    dfs(root);
    
    cout << max(f[root][0], f[root][1]) << endl;
    
    return 0;
}

285.没有上司的舞会

记忆化搜索

滑雪

  • 状态表示 f [ i , j ] f[i,j] f[i,j]:所有从(i,j)出发的最长路径
  • 状态计算: f [ i , j ] = m a x ( f [ i , j − 1 ] + 1 , f [ i , j + 1 ] + 1 , f [ i − 1 , j ] + 1 , f [ i + 1 , j ] + 1 ) f[i,j] = max(f[i,j-1]+1,f[i,j+1]+1,f[i-1,j]+1,f[i+1,j]+1) f[i,j]=max(f[i,j1]+1,f[i,j+1]+1,f[i1,j]+1,f[i+1,j]+1)

枚举出上下左右的4个点,(i,j)由它们的最大值转移而来

如果一个点的dp[][]不为初始化值-1,那么就说明计算过,不必再次计算。这也是记忆化搜索的一个特点。

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

using namespace std;

const int N = 310;
int n, m;
int f[N][N], h[N][N];

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

int dp(int x, int y) {
    // 引用:v相当于f[x][y]
    int &v = f[x][y];
    if (v != -1) return v;
    
    // 无格可走,最小为1
    v = 1;
    
    for (int i = 0; i < 4; i++) {
        int a = x + dx[i], b = y + dy[i];
        
        if (a >= 1 && a <= n && b >= 1 && b <= m && h[a][b] < h[x][y]) 
            v = max(v, dp(a, b) + 1);
    }
    
    return v;
}

int main() {
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> h[i][j]; // 读入高度
    
    // 初始化状态为-1
    memset(f, -1, sizeof f);
    
    int res = 0;
    // 枚举每一个点
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            res = max(res, dp(i, j));
    
    cout << res << endl;
    
    return 0;
}

901.滑雪

参考资料

AcWing算法基础课,欢迎大家报名,强推噢!!!y总带你感受算法之美!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

CodeSlogan

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

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

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

打赏作者

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

抵扣说明:

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

余额充值