动态规划DP题单 AcWing算法基础课 (详解)

背包问题

背包问题初始化总结

在这里插入图片描述
总的来说就是 :求方案数, f [ 0 ] = 1 f[0]=1 f[0]=1;求最大/最小值 : f [ 0 ] = 0 f[0]=0 f[0]=0

AcWing 2. 01背包问题

在这里插入图片描述

#include <bits/stdc++.h>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 1e3 + 10;

int f[N];

int main()
{
    IOS;
    
    int n, m;
    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;
}

AcWing 3. 完全背包问题

在这里插入图片描述
其实就是把01背包j循环层倒过来写啊(

#include <bits/stdc++.h>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 1e3 + 10;

int f[N];

int main()
{
    IOS;
    
    int n, m;
    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;
}

AcWing 4. 多重背包问题

在这里插入图片描述
数据范围非常小,暴力枚举

#include <bits/stdc++.h>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 110;

int f[N];

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

AcWing 5. 多重背包问题 II

在这里插入图片描述

二进制表示法 : 把 s s s拆成 l o g ( s ) log(s) log(s)份就可以表示出0 ~ s s s的所有的数,不再需要s份(s个1)
原来的复杂度是 O ( N ∗ V ∗ S ) O(N * V * S) O(NVS),现在是 O ( N ∗ V ∗ l o g ( S ) ) O(N * V * log(S)) O(NVlog(S))

#include <bits/stdc++.h>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 2e3 + 10;

struct Good
{
    int v, w;
};

int f[N];

int main()
{
    IOS;
    
    int n, m;
    cin >> n >> m;
    
    vector<Good> goods;
    
    for (int i = 0; i < n; i ++ )
    {
        int v, w, s;
        cin >> v >> w >> s;
        
        for (int k = 1; k <= s; k *= 2)
        {
            goods.push_back({k * v, k * w});
            s -= k;
        }
        if (s > 0) goods.push_back({s * v, s * w});
    }
    
    // 得到了我们需要的物品组 :goods,问题转化为了01背包
    for (auto good : goods)
        for (int j = m; j >= good.v; j -- )
            f[j] = max(f[j], f[j - good.v] + good.w);
    
    cout << f[m] << endl;
    
    return 0;
}

AcWing 9. 分组背包问题

在这里插入图片描述

/*
 for (int i = 0; i < n; i ++ )
    for (int j = m; j >= 0; j -- )
        f[j] = max(f[j], f[j - v[0]] + w[0], f[j - v[1]] + w[1], ..., f[j - v[s - 1]] + w[s - 1]);
 即 f[i][j] 表示只考虑前i组物品,体积至多为j的选法集合
 每个组内有s + 1种决策
 即 每个组内选1个(包含0个的情况)与其他组选出来的进行01背包
 */

#include <bits/stdc++.h>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 110;

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

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

线性DP

AcWing 898. 数字三角形

在这里插入图片描述

  • 路线问题,一般状态表示可以用坐标
  • f [ i ] [ j ] f[i][j] f[i][j]表示从底向上走到 ( i , j ) (i, j) (i,j)的所有路线的集合,属性是 m a x max max
  • f [ i ] [ j ] = m a x ( f [ i + 1 ] [ j ] , f [ i + 1 ] [ j + 1 ] ) + w [ i ] [ j ] f[i][j] = max(f[i+1][j],f[i+1][j+1])+w[i][j] f[i][j]=max(f[i+1][j],f[i+1][j+1])+w[i][j]
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <deque>
#include <ctime>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 510;

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

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

AcWing 895. 最长上升子序列

在这里插入图片描述

  • f [ i ] [ j ] f[i][j] f[i][j]表示所有以第i个数结尾的上升子序列,属性是 m a x max max
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <deque>
#include <ctime>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 1010;

int a[N], f[N];

int main()
{
    int n;
    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;
}

AcWing 896. 最长上升子序列 II(贪心)

在这里插入图片描述

  • 相同长度的上升子序列中结尾的数字越小越好,所以我们想到记录不同长度下同一长度的上升子序列结尾数字的最小值。
  • 会发现,这个序列是单调递增的。每个插入一个新的数,可以二分找到小于它的数中最大的数,即 q j < a i q_j<a_i qj<ai,又有 q j + 1 > = a i q_{j+1}>=a_i qj+1>=ai,把 a i a_i ai接在 q j q_j qj后面,再用 a i a_i ai代替 q j + 1 q_{j+1} qj+1
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <deque>
#include <ctime>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 1e5 + 10;

int a[N], q[N];

int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> a[i];
    
    int len = 0;        // 当前最大长度 / q中元素个数
    q[0] = -2e9;        // 二分的边界问题,为了保证数组中小于某个数的最大的数一定存在,q[0]是一个哨兵
    
    for (int i = 0; i < n; i ++ )
    {
        int l = 0, r = len;
        while (l < r)
        {
            // 找比a[i]小的中最大的
            int mid = l + r + 1 >> 1;
            if (q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r + 1);
        q[r + 1] = a[i];
    }
    
    cout << len << endl;
    return 0;
}

AcWing 897. 最长公共子序列

在这里插入图片描述

  • f [ i ] [ j ] f[i][j] f[i][j]表示 A [ 1 , i ] , B [ 1 , j ] A[1, i],B[1,j] A[1,i],B[1,j]中公共子序列的集合,属性是公共子序列最长的长度,即求 m a x max max
  • 根据 a [ i ] , b [ j ] a[i],b[j] a[i],b[j]是否在公共子序列中可以将 f [ i ] [ j ] f[i][j] f[i][j]划分成 00 , 10 , 01 , 11 00,10,01,11 00,10,01,11,分别对应 f [ i − 1 ] [ j − 1 ] , f [ i ] [ j − 1 ] , f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − 1 ] + 1 f[i-1][j-1],f[i][j-1],f[i-1][j],f[i-1][j-1]+1 f[i1][j1],f[i][j1],f[i1][j],f[i1][j1]+1
  • 其实中间两种情况用这两个代替是有重复的,因为 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j]本身包含 s [ j ] s[j] s[j]本身在公共子序列中和不在公共子序列中两种情况,但由于求最大值,所以重复没关系,且 s [ j ] s[j] s[j]本身不是公共子序列的情况也包含在 f [ i ] [ j ] f[i][j] f[i][j]
  • f [ i − 1 ] [ j − 1 ] f[i-1][j-1] f[i1][j1]包含在后两者的情况中,所以可以省略。
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <deque>
#include <ctime>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 1010;

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

int main()
{
    int n, m;
    cin >> n >> m >> 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]);
            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;
}

AcWing 902. 最短编辑距离

在这里插入图片描述

  • f [ i ] [ j ] f[i][j] f[i][j]表示所有将 a [ 1 , i ] a[1,i] a[1,i]变成 b [ 1 , j ] b[1,j] b[1,j]的操作方式,属性为 m i n min min
  • “增”“删”“改”分别对应 f [ i ] [ j ] = m i n ( f [ i − 1 ] [ j ] + 1 , f [ i ] [ j − 1 ] + 1 , f [ i − 1 ] [ j − 1 ] + 1 / 0 ) f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1,f[i-1][j-1]+1/0) f[i][j]=min(f[i1][j]+1,f[i][j1]+1,f[i1][j1]+1/0)
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <deque>
#include <ctime>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 1010;

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

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

AcWing 899. 编辑距离

在这里插入图片描述
注意头文件。

#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 15, M = 1010;

char str[M][N];
int f[N][N];

int edit_distance(char a[], char b[])
{
    int lena = strlen(a + 1), lenb = strlen(b + 1);
    
    for (int i = 0; i <= lena; i ++ ) f[i][0] = i;
    for (int i = 0; i <= lenb; i ++ ) f[0][i] = i;
    
    for (int i = 1; i <= lena; i ++ )
        for (int j = 1; j <= lenb; j ++ )
        {
            f[i][j] = min(f[i - 1][j], f[i][j - 1]) + 1;
            if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
            else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
        }
    
    return f[lena][lenb];
}

int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < n; i ++ ) cin >> str[i] + 1;
    
    while (m -- )
    {
        char s[N];
        int limit;
        cin >> s + 1 >> limit;
        
        int res = 0;
        for (int i = 0; i < n; i ++ )
            if (edit_distance(s, str[i]) <= limit) res ++ ;
        
        cout << res << endl;
    }
    
    return 0;
}

区间DP

AcWing 282. 石子合并( m i n min min

在这里插入图片描述

  • 每次合并相邻两堆,因此不是贪心问题。
  • 所有不同的合并顺序显然有 ( n − 1 ) ! (n-1)! (n1)!(因为每次相邻两个)。
  • 相邻,因此最后必然是将左一堆和右一堆合并的情形,因此联想到 f [ i , j ] f[i,j] f[i,j]表示将区间 [ i , j ] [i,j] [i,j]合并成一堆的方案的集合,属性是 m i n min min,而分类依据就是两堆的分界点,由于左右两堆分别独立,要总的最小,就分别取最小。这样 f [ i , j ] f[i,j] f[i,j]一个状态就可以表示 ( j − i ) ! (j-i)! (ji)!个方案。
  • O ( n 2 ∗ k ) = = O ( n 3 ) O(n^2*k)==O(n^3) O(n2k)==O(n3)
  • 由于求最小值,因此注意初始化。
  • 要枚举不同长度的子序列(连续),可以第一层枚举长度。
  • 状态计算 :i < j;i = j时,f[i][i] = 0(合并一堆石子代价为0)
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <deque>
#include <ctime>
#include <sstream>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 310;

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

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

  • 记忆化搜索 :
    在这里插入图片描述

计数类DP

AcWing 900. 整数划分(完全背包求方案数,体积恰好)

在这里插入图片描述

  • 由于不考虑组合的数的顺序,因此直接就是完全背包问题了。从1-i中选,体积恰好为j的方案集合。
  • f [ i , j ] = f [ i − 1 , j ] + f [ i − 1 , j − i ] + f [ i − 1 , j − 2 i ] + . . . f[i,j]=f[i-1,j]+f[i-1,j-i]+f[i-1,j-2i]+... f[i,j]=f[i1,j]+f[i1,ji]+f[i1,j2i]+...
  • f [ i , j − i ] = f [ i − 1 , j − i ] + f [ i − 1 , j − 2 i ] + . . . f[i,j-i]=f[i-1,j-i]+f[i-1,j-2i]+... f[i,ji]=f[i1,ji]+f[i1,j2i]+...
  • 因此, f [ i , j ] = f [ i , j − i ] + f [ i − 1 , j ] f[i,j]=f[i,j-i]+f[i-1,j] f[i,j]=f[i,ji]+f[i1,j],即, f [ j ] = f [ j − i ] + f [ j ] f[j]=f[j-i]+f[j] f[j]=f[ji]+f[j],且体积层从小到大循环。
  • 神奇的是虽然这题结果可能很大,需要取模,但不需要long long。
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <deque>
#include <ctime>
#include <sstream>
#define endl '\n'
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define lowbit(x) (x&-x)
using namespace std;
const double pi = acos(-1);
typedef long long ll;
typedef pair<int, int> PII;
typedef pair<long, long> PLL;

const int N = 1e3 + 10, mod = 1e9 + 7;

int f[N];

int main()
{
    int n;
    cin >> n;
    
    f[0] = 1;
    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

AcWing 338. 计数问题

在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

int get(int n) { // 求数n的位数
    int res = 0;
    while (n) res ++, n /= 10;
    return res;
}

int count(int n, int i) { // 求从1到数n中数i出现的次数
    int res = 0, dgt = get(n);

    for (int j = 1; j <= dgt; ++ j) {
    /* p为当前遍历位次(第j位)的数大小 <10^(右边的数的位数)>,
        l为第j位的左边的数,r为右边的数,dj为第j位上的数 */
        int p = pow(10, dgt - j), l = n / p / 10, r = n % p, dj = n / p % 10;

   // ps:下文的xxx、yyy均只为使读者眼熟,并不严格只是三位数啊~ 然后后续的...就代表省略的位数啦~
        /* 求要选的数在i的左边的数小于l的情况:
(即视频中的xxx1yyy中的xxx的选法) --->
    1)、当i不为0时 xxx : 0...0 ~ l - 1, 即 l * (右边的数的位数) == l * p 种选法
    2)、当1位0时 由于不能有前导零 故xxx: 0....1 ~ l - 1, 
                    即 (l-1) * (右边的数的位数) == (l-1) * p 种选法 */
        if (i) res += l * p;
        else res += (l - 1) * p;

        /* 求要选的数在i的左边的数等于l的情况:(即视频中的xxx == l 时)
    (即视频中的xxx1yyy中的yyy的选法)--->
                        1)、i > dj时 0种选法
                        2)、i == dj时 yyy : 0...0 ~ r 即 r + 1 种选法
                        3)、i < dj时 yyy : 0...0 ~ 9...9 即 10^(右边的数的位数) == p 种选法 */
        if (i == dj) res += r + 1;
        if (i < dj) res += p;
    }

    return res; // 返回结果
}

int main() {
    int a, b;
    while (cin >> a >> b, a) { // 输入处理,直到输入为0停止
        if (a > b) swap(a, b); // 预处理-->让a为较小值,b为较大值
        for (int i = 0; i <= 9; ++ i) cout << count(b, i) - count(a - 1, i) << ' '; 
        // 输出每一位数字(0 ~ 9)分别在[a,b]中出现的次数<利用前缀和思想:[l,r]的和=s[r] - s[l - 1]>
        cout << endl; //换行
    }

    return 0; // 惯例:结束快乐~
}

状态压缩DP

AcWing 291. 蒙德里安的梦想

在这里插入图片描述

  • 放方块时,先放横着的再放竖着的,总方案数等于只放横着的方块的合法方案数。
  • 合法方案数 即 所有剩余位置能用竖着的方块填满。从每列来看,每列内部所有连续的空着的方块是偶数个。
  • 用一个 N N N位的二进制数,每一位表示一个物品,0/1表示不同状态,因此可以用0~ 2 n − 1 2^n-1 2n1
    中的数来枚举全部的状态。
  • f [ i , j ] f[i,j] f[i,j]表示已将前 i − 1 i-1 i1列摆好,且从第 i − 1 i-1 i1列伸出到第 i i i列的状态是 j j j的所有方案,其中 j j j是一个二进制数,用来表示哪一行的方块是横着放的,其位数和棋盘行数一致。
#include <iostream>
#include <vector>
#include <cstring>

using namespace std;

const int N = 12, M = 1 << N;       // M = 2^N

typedef long long LL;

LL f[N][M];     // 第一维表示列, 第二维表示所有可能的状态

bool st[M];     // 存储每种状态是否有奇数个连续的0, 如果奇数个0则是无效状态,如果是偶数个零置为true

int n, m;

// vector<int> state[M];        //  二维数组记录合法的状态
vector<vector<int>> state(M);       // 两种写法等价 : 二维数组

int main()
{
    while (cin >> n >> m, n || m)
    {

        // 第一部分 : 预处理1
        // 对于每种状态,先预处理每列不能有奇数个连续的0

        for (int i = 0; i < 1 << n; i ++ )      // i  ->  [0, 2 ^ n - 1]
        {
            int cnt = 0;        // 记录连续的0的个数

            bool isValid = true;        // 某种状态没有奇数个连续的0则为true

            for (int j = 0; j < n; j ++ )       // 遍历这一列, 从上到下
            {
                if (i >> j & 1)     // i >> j位运算,表示i(i在此处是一种状态)的二进制数的第j位,如果这第j位是1,则进入if
                {
                    if (cnt & 1)        //  如果这一位是1,看前面连续的0的个数,如果是奇数(cnt & 1 为真)则不合法
                    {
                        isValid = false;
                        break;
                    }
                    cnt = 0;        //  既然该位是1,则到这里0就断了, cnt清零
                }
                else cnt ++ ;
            }
            if (cnt & 1) isValid = false;       // 最下面的那一段判断连续的0的个数

            st[i] = isValid;        // 状态i是否有奇数个连续的0的情况,输入到st数组中
        }

        // 第二部分 : 预处理2
        // 经过上面每种状态 连续0的判断,已经筛掉一些状态
        // 下面来看进一步的判断 : 看第i - 2列渗出来的和第i - 1列伸出去的是否冲突

        for (int j = 0; j < 1 << n; j ++ )      //  对于第i列的所有状态
        {
            state[j].clear();       // 清空上次操作遗留的状态,防止影响本次状态
            for (int k = 0; k < 1 << n; k ++ )      // 对于第i - 1列的所有状态
            {
                if ((j & k) == 0 && st[j | k])      // 第i - 2列和第i - 1列伸出来的不冲突(不在同一行)
                    // (j & k) == 0 表示j和k没有一位同时为1,即没有一列有冲突
                    
                    // 解释一下st[j | k] :
                    // 已经知道st[]数组表示的是这一列没有奇数个连续的0的情况
                    // 我们要考虑的是第i - 1列(第i - 1列是这里的主体)中从第i - 2列中横插过来的和第i - 1列横插到第i列的
                    // 比如第i - 2列插到第i - 1列的是k = 10101, 第i - 1列插去第i列的是 j = 01000
                    // 那么合在第i - 1列,到底有多少个1呢 ?自然想到的是这两个操作共同的结果 :两个状态或(都是0才是0)。 j | k == 01000 | 10101 == 11101
                    // 这个j | k就是当前第i - 1列的到底有几个1,即哪几行是横着放格子的

                    state[j].push_back(k);
                    // j 表示第i列"真正"可行的状态, 如果第i - 1列的状态k和j不冲突则压入state数组中的第j行
                    // "真正"可行是指 : 既没有前后两列伸进伸出的冲突;又没有奇数个连续的0
            }
        }

        // 第三部分 :dp开始

        memset(f, 0, sizeof f);     // 全部初始化为0,因为是连续读入,这里是一个清空操作

        f[0][0] = 1;        // 回忆状态表示的定义 : 前i - 1列都已经摆好,且从第-1列第0列伸出来的状态为0的方案数
        // 首先这里没有第-1列,最少也是第0列。其实,没有伸出来的,就是没有横着摆的,即这个第0列只有竖着摆的这1种状态

		// 由于前i-1列已经放好 的 这个定义,所以是从第一行开始而不是第零行
        for (int i = 1; i <= m; i ++ )      // 遍历每一列
            for (int j = 0; j < 1 << n; j ++ )      // 遍历当前列(第i列)的所有状态
                for (auto k : state[j])     // 遍历第i - 1列的状态k,如果"真正"可行,就转移
                    f[i][j] += f[i - 1][k];     // 当前列的方案数就等于之前的第i - 1列所有状态k的累加


        // 最后答案是什么呢?
        // f[m][0]表示前m - 1列都已经处理完,并且第m - 1列没有伸出来的方案数
        // 即整个棋盘处理完的方案数

        cout << f[m][0] << endl;
    }
}

AcWing 91. 最短Hamilton路径

在这里插入图片描述

  • H a m i l t o n Hamilton Hamilton路径 :把每个点经过一次且只经过一次
  • 从0走到 n − 1 n-1 n1 :首先确定走到顺序 n ! n! n!,计算路径长度 : n ! ∗ n n! * n n!n
  • f [ i , j ] f[i,j] f[i,j]表示已经走过的所有点是 i i i,已经走到了 j j j点的所有路径。
  • 根据倒数第二个点是什么进行分类。
#include <iostream>
#include <cstring>

using namespace std;

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

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 = 0; i < 1 << n; i ++ )
        for (int j = 0; 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;
}

树形DP

AcWing 285. 没有上司的舞会

在这里插入图片描述

  • 翻译 :选了某个节点就不能选择它的父节点和子节点,求最大权值和。
#include <iostream>
#include <cstring>

using namespace std;

const int N = 6010;

int n;
int e[N], ne[N], h[N], idx;
int w[N];
int f[N][2];
bool st[N];     // 是否有父节点

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

void dfs(int u)
{
    // f[u][0] = 0;
    f[u][1] = w[u];

    for (int i = h[u]; ~i; i = ne[i])       // ~i   ==  i != -1
    {
        int j = e[i];

        dfs(j);

        // 两种情况都是 +=
        f[u][1] += f[j][0];
        f[u][0] += max(f[j][1], f[j][0]);
    }
}

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

    memset(h, -1, sizeof h);		// 不初始化图见祖宗

    for (int i = 1; i <= n - 1; i ++ )
    {
        int a, b;
        cin >> a >> b;
        add(b, a);
        st[a] = true;
    }

    int root = 1;
    while (st[root]) ++ root;

    dfs(root);

    cout << max(f[root][0], f[root][1]) << endl;

    return 0;
}

记忆化搜索

AcWing 901. 滑雪

在这里插入图片描述

  • f [ i , j ] f[i,j] f[i,j]表示所有从 ( i , j ) (i,j) (i,j)开始滑的路径
#include <iostream>
#include <cstring>

using namespace std;

const int N = 310;

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

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

int dp(int x, int y)
{
    int &v = f[x][y];       // 使用"&",相当于下面每次用到v其实可以换成f[x][y]
    if (v != -1) return v;		// 记忆化搜索

    v = 1;

    for (int i = 0; i < 4; i ++ )
    {
        int a = x + dx[i], b = y + dy[i];
        if (a >= 0 && a < n && b >= 0 && 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 = 0; i < n; i ++ )
        for (int j = 0; j < m; j ++ )
            cin >> h[i][j];

    memset(f, -1, sizeof f);

    int res = 0;
    for (int i = 0; i < n; i ++ )
        for (int j =0 ; j < m; j ++ )
            res = max(res, dp(i, j));

    cout << res << endl;

    return 0;
}
  • 2
    点赞
  • 22
    收藏
  • 打赏
    打赏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

5pace

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值