Acwing基础课记录篇——动态规划

前言

本文以代码模板为主,加上个人的代码的理解和注意事项,方便复习和再次回忆补充,旨在提高记忆和运用模板能力,代码部分均为转载Acwing平台,二次使用请标明出处

背包问题

01背包

1. 每个物品只使用一次,总价值最大
2. 一维空间的优化下,选和不选都是从上一层来转移
3. 在计算f[j]时唯二的状态中j(不选) > j - v[i](选),j - v[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]);
完全背包

1. 每个物品都可无限使用,总价值最大
2. 一维空间的优化下,max(不选,选1种,选2种……选n种),f[i][j] = 不选的f[i - 1][j] + 选n种(n > 0)的f[i - 1][j - v[i] * n],后者就是f[i][j - v[i]]
3. 考虑f[j]值更大后更新,所以采用正序遍历

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]);
多重背包

1. 每个物品的有限使用,总价值最大
2. 容量遍历额外加一层次数循环k,不超过容量下从0选到物品对应次数

 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);
 多重背包II(数量二进制)

1. 每个物品的有限使用,总价值最大,物品N-1000、容量V-2000、数量s-2000,重点对于第三重循环s的优化
2. 数量s可以用二进制倍数和一个余数至多log2000个数表示,如200={1,2,4,8,16,32,64,73},枚举物品状态的复杂度是1000 * log2000 ≈ 11010,容量是2000
3. 每个物品的次数s与常数k比较,k每轮*2左移并将k倍体积和价值放进物品状态,s减至小于k时余数最后一次放入,一维数组优化,考虑j - v[i]值更小需要后更新,倒序遍历

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;

    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]);
分组背包

1. n组物品,每组物品只能使用一个,总价值最大
2. 外层循环从前面的遍历物品数改为遍历组,内层遍历容量和每组的物品
3. 每组只能选用一个,考虑j - v[i][k]值更小需要后更新,倒序遍历

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]);

线性DP

数字三角形

1. 从数字三角形顶部走到底层,路径数字和最大
2. 从倒数第二层开始,每层都加其下方左右两个节点更大值,对于(i,j)来说就是加(i +1,j)和(i + 1, j + 1)的更大值,到根节点就是答案

for(int i = 1; i <= n; i++)
        for(int j = 1; j <= i; j++)
            cin >> f[i][j];
            
    for(int i = n - 1; i >= 1; i--) 
        for(int j = 1; j <= i; j++)
            f[i][j] += max(f[i + 1][j], f[i + 1][j + 1]);
最长上升子序列

1. 求数组严格递增的子序列最长长度
2. f[i]以节点i为结尾的序列最长长度,遍历前面结尾小于本节点且长度最长的子序列并+1,计算完遍历以每个点结尾的长度取最大就是答案
3. 最短序列只有自己为1

for (int i = 1; i <= n; i ++ )
    {
        f[i] = 1; // 只有a[i]一个数
        for (int j = 1; j < i; j ++ )
            if (a[j] < a[i])
                f[i] = max(f[i], f[j] + 1);
    }
最长上升子序列II(时间优化)

1. 求数组严格递增的子序列最长长度
2. 遍历序列的每个元素
        (1) 找到的值是结果序列的最后一个元素(空情况也满足),更新结果序列长度和序列最后一个元素
        (2) 找到的值是结果序列中的元素,证明找到的值后面的元素一定大于目前遍历的元素,维持长度不变,将遍历的元素替代找到的值后面的元素
3. 左开右闭的二分查找找到结果序列内小于等于该元素的最大值,r是该元素停留位置

int len = 0;
    for (int i = 0; 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;
        }
        len = max(len, r + 1);
        q[r + 1] = a[i];
    }
最长公共子序列

1. 求两个字符串共同的子序列最长长度
2. f[i][j]表示a的前i个字符和b的前j个字符的最长子序列长度,两个字符相等,转移到f[i - 1][j - 1] + 1,不相等一定有一个可以抛弃,对f[i - 1][j]和f[i][j - 1]取max来转移
3. a和b两重循环,下标从1开始

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (a[i] == b[j]) {
        f[i][j] = f[i - 1][j - 1] + 1;
      } else {
        f[i][j] = max(f[i - 1][j], f[i][j - 1]);
      }
    }
  }
最短编辑距离

1. 字符串A经过删、加、改转移成字符串B,求转移的最小步数
2. f[i][j]表示从a的前i个字符转移到b的前j个字符的最小步数,遍历的两个字符相等,转移到f[i - 1][j](删),f[i][j - 1](加),f[i - 1][j - 1],不相等f[i - 1][j - 1]改成f[i - 1][j - 1] + 1(改)
3. 初始化f[i][0]和f[0][j]转移到空字符串的步数,a和b两重循环,下表从1开始

for (int i = 0; i <= m; i ++ ) f[0][i] = i;
    for (int i = 0; i <= n; i ++ ) f[i][0] = i;

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
            else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
        }
编辑距离

1. 给定n个字符串和m次询问(字符串和操作上限),求n个字符串中在操作上限内能转移到询问字符串的个数
2. 每次询问遍历每个字符串和询问的字符串,以两个字符串的长度作为f[i][j]转移长度的上限
3. 步数小于限制结果++,a和b两重循环,下表从1开始

int la = strlen(a + 1), lb = strlen(b + 1);

    for (int i = 0; i <= lb; i ++ ) f[0][i] = i;
    for (int i = 0; i <= la; i ++ ) f[i][0] = i;

    for (int i = 1; i <= la; i ++ )
        for (int j = 1; j <= lb; j ++ )
        {
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
        }

区间DP

石子合并

1. n堆石子每次合并相邻两堆,合并代码是两堆质量之和,求总代价最小
2. f[i][j]是将第i堆到第j堆合并成一堆的最小合并代价,计算以k为分界,[l, k]和[k + 1,r]两个区间最小,最后合并两个最小子区间代价用r - (l - 1)的前缀和计算
3. 前缀和初始化,遍历长度和左边界(右边界为i + len - 1),代价初始化为无穷,遍历分界点k(k + 1 <= r故分界点边界< r)

for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];

    for (int len = 2; len <= n; 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]);
        }

计数类DP

整数划分

1. 正整数n可以拆分成若干个正整数之和,求n的划分方法总数,结果对1e9 + 7取模
2. 方案一f[i][j]表示从1~i中选,总和等于j的方案数,类比容量为n,在n范围内选的次数无限的完全背包,f[1] = f[0] + f[1]故f[0]初始赋值为1

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;

3. 方案二f[i][j]表示总和为i,总个数为j的方案数,f[i][j]转移到选法最小为1的f[i - 1][j - 1] + 选法最小大于1的f[i - j][j],遍历累加总和为n的1~n的个数的方案数

f[1][1] = 1;
    for (int i = 2; 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;

数位统计DP

计数问题

1. 给定两个整数a和b,求a和b之间所有数字中0~9的出现次数
2. count()函数计算1~n中x的出现次数,将n的每位存入数组,从最高位开始遍历每位为x时的总数,以x = 2且在第4位出现,n = abcdefg为例,1~abcdefg出现的数分为前三位< abc和= abc两种
        (1) < abc,前三位范围000~abc - 1,后三位范围(任选)为000~999,总数abc * 1000
        (2) = abc
                + d < 2,不存在形如abc2efg的数,总数0
                + d = 2,后三位范围000~efg,总数efg + 1
                + d > 3,后三位范围(任选)000~999,总数1000

3. 遍历最高位时不存在(1)情况前面没数,(1)的计算从n - 2位开始
4. x = 0时在次高位开始遍历,i = n - 1 - !x,且x = 0时(1)情况前三位范围是001~abc - 1(没有形如0000efg的数),if (!x) res -= power10(i)减去这种情况
5. get将[l,r]数组元素转为原数,power10计算10^x次方,当a > b时交换从更小数开始,遍历0~9计算1~b的出现次数 - 1 ~  a - 1的出现次数(前缀和)就是结果

int count(int n, int x)
{
    if (!n) return 0;

    vector<int> num;
    while (n)
    {
        num.push_back(n % 10);
        n /= 10;
    }
    n = num.size();

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

状态压缩DP

蒙德里安的梦想

1. 将n * m的棋盘可以摆放不同的1 * 2长方形的方案
2. f[i][j]表示i - 1列已放好,当前列状态是j的方案数,j为二进制数形如00100表示第3行从i - 1列伸出到i列,预处理出可以竖着放(偶数0)的状态,按列放横着的方案数就是结果
3. 预处理2^n种选法中不存在奇数0的合法状态,合法状态的基础上计算每个状态可以转移的状态,满足(i & j) == 0(没有冲突的行) && st[i | j](合并后仍是合法状态),按列遍历每个状态累加它对应的合法状态数就是结果
4. 每个状态的合法转移状态vector<int> state[M]用vector数组存储,会有不同的棋盘故state在每次更新合法转移状态时清空上次结果,f[0][0]让f[1][0]合法转移初始为1,f[m][0]第m - 1列摆好且没有伸出来的选法就是结果

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)
    {
        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;
                    }
                    cnt = 0;
                }
                else cnt ++ ;
            if (cnt & 1) is_valid = false;
            st[i] = is_valid;
        }

        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;
}
最短Hamilton路径

1. 给定n个点的带权无向图,点从0~n - 1开始,求0~n - 1不重不漏地经过每个点恰好一次的最短路径
2. f[i][j]表示i状态下(i是1表示走过点的二进制数),终点是j的最短路径,遍历2^n种选法中的终点,再遍历倒数第二个点,对f[i][j]和f[i - (1 << j)][k]两种状态取min来尝试用倒数第二个点更新
3. 默认从0开始,i = 1表示走过0,f[1][0]初始化为1,在遍历终点和倒数第二个点都需要保证点是走过的分别用i >> j & 1和去掉终点看倒数第二个点的i - (1 << j) >> k & 1判断

for(int i = 1; i < 1 << n; i += 2)
        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] + weight[k][j]);

树形DP

没有上司的舞会

1. 大学中有n名职员编号1~n,关系是以校长为根的树,父节点是子节点的直接上司,每个职员都有一个快乐指数,每个职员和上司一起参会时快乐指数为0,求最大快乐指数
2. f[i][j]表示选或不选根节点i的快乐指数(j用0表示不选1,表示选),当选根节点时转移到子节点u不选f[u][0],不选根节点时对子节点f[u][1]和f[u][0]取max来转移
3. 需要从根节点开始深搜到每个节点,根节点通过输入时有父节点的加入bool数组,没有父节点的就是根节点,树使用图论模板添加单向边,深搜开头初始化选择根节点状态为自身快乐指数,对选或不选根节点取max就是结果

void dfs(int u)
{
    f[u][1] = happy[u];

    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dfs(j);

        f[u][1] += f[j][0];
        f[u][0] += max(f[j][0], f[j][1]);
    }
}

记忆化搜索

滑雪

1. 给定r行c列的矩阵,矩阵的点表示区域的高度,一个人从某个区域出发,可以向上下左右任一方向滑动一个单位,滑动的前提是该区域小于目前所在区域,求滑雪的最长距离
2. f[i][j]表示从[i,j]开始的最长距离,转移到上下左右移动后的f[a][b] + 1
3. 记忆化搜索函数dp已经计算过的节点直接返回它的最长距离,遍历dx和dy坐标移动数组更新合法移动的最长距离,遍历全部的点出发的最长距离就是结果

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

int dp(int x, int y)
{
    int &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 >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b])
            v = max(v, dp(a, b) + 1);
    }

    return v;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值