Acwing算法基础课学习笔记(十六)--动态规划之计数类DP&&数位统计DP&&状态压缩DP&&树形DP&&记忆化搜索

整数划分
将其转换为完全背包问题:
在这里插入图片描述
朴素做法:

//f[i][j] = f[i - 1][j] + f[i][j - i]
#include <iostream>

using namespace std;

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

int f[N][N];

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

    for (int i = 0; i <= n; i ++) {
        f[i][0] = 1; // 容量为0时,前 i 个物品全不选也是一种方案
    }

    for (int i = 1; i <= n; i ++) {
        for (int j = 0; j <= n; j ++) {
            f[i][j] = f[i - 1][j] % mod; // 特殊 f[0][0] = 1
            if (j >= i) f[i][j] = (f[i - 1][j] + f[i][j - i]) % mod;
        }
    }

    cout << f[n][n] << endl;
}

等价变形:

// f[i][j] = f[i - 1][j] + f[i][j - i]
//f[j] = f[j] + f[j - i]
#include <iostream>
using namespace std;

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

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

计数问题
小学数奥问题,重点在于分情况讨论! 基本的思想是剑指offer上的一道题(求1~n中1
出现的次数)。
推荐题解

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

using namespace std;

const int N = 10;

/*

001~abc-1, 999

abc
    1. num[i] < x, 0
    2. num[i] == x, 0~efg
    3. num[i] > x, 0~999

*/

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;
}

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

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;
}

int main()
{
    int a, b;
    while (cin >> a >> b , a)
    {
        if (a > b) swap(a, b);

        for (int i = 0; i <= 9; i ++ )
            cout << count(b, i) - count(a - 1, i) << ' ';
        cout << endl;
    }

    return 0;
}

蒙德里安的梦想
也是一个难度比较大的题目,做法是状态压缩DP。所谓的状态压缩DP,就是用二进制数保存状态。为什么不直接用数组记录呢?因为用一个二进制数记录方便作位运算。前面做过的八皇后,八数码,也用到了状态压缩。本题等价于找到所有横放 1 X 2 小方格的方案数,因为所有横放确定了,那么竖放方案是唯一的。用f[i][j]记录第i列第j个状态。j状态位等于1表示上一列有横放格子,本列有格子捅出来。转移方程很简单,本列的每一个状态都由上列所有“合法”状态转移过来f[i][j] += f[i - 1][k].两个转移条件: i 列和 i - 1列同一行不同时捅出来 ; 本列捅出来的状态j和上列捅出来的状态k求或,得到上列是否为奇数空行状态,奇数空行不转移。初始化条件f[0][0] = 1,第0列只能是状态0,无任何格子捅出来。返回f[m][0]。第m + 1列不能有东西捅出来。
参考题解

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

using namespace std;

const int N = 12, M = 1 << N;//M的每一位二进制位储存一种状态

int n, m;
long long f[N][M];
bool st[M];//储存每一列上合法的摆放状态

//f[i][j]表示摆放第i列,i列向后伸出来横着的方格状态为j的方案数,j为一个二进制数,用01表示是否戳出来
int main()
{
    while (cin >> n >> m, n || m)
    {
        //枚举每一列的占位状态里哪些是合法的
        for (int i = 0; i < 1 << n; i ++ )//一共n行,枚举n位不同的状态
        {
            int cnt = 0;//用来记录连续的0的个数
            st[i] = true;//记录这个状态被枚举过且可行
            for (int j = 0; j < n; j ++ )//从低位到高位枚举它的每一位
                if (i >> j & 1)//如果为1
                {
                    if (cnt & 1) st[i] = false;//如果之前连续0的个数是奇数,竖的方块插不进来,这种状态不行
                    cnt = 0;//清空计数器
                }
                else cnt ++ ;//如果不为1,计数器+1
            if (cnt & 1) st[i] = false;//到末尾再判断一下前面0的个数是否为奇数,同前
        }

        memset(f, 0, sizeof f);//一定要记得初始化成0,对于每个新的输入要重新计算f[N][M]
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ )//对于每一列
            for (int j = 0; j < 1 << n; j ++ )//枚举j的状态
                for (int k = 0; k < 1 << n; k ++ )//再枚举前一行的伸出状态k
                    if ((j & k) == 0 && st[j | k])//如果它们没有冲突,i这一列被占位的情况也是合法的话
                        f[i][j] += f[i - 1][k];//那么这种状态下它的方案数等于之前每种k状态数目的和

        cout << f[m][0] << endl;//求的是第m行排满,并且第m行不向外伸出块的情况
    }
    return 0;
}

最短Hamilton路径
依旧是一道状态压缩dp。
参考题解

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

using namespace std;

const int N = 20, M = 1 << N;// 一共最多有 20 个 1 种状态

int n;
int w[N][N]; // 存每两个点之间的距离
//从所有0走到j,走过的所有点是i的所有路径;i表示一个二进制数,表示当前这个点是不是走过了;上述 f[state][j]
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);//由于要求最小值,所以这里将 f 初始化为正无穷会更好处理一些
    f[1][0] = 0;// 因为要求从点 0 出发,所以这里要将 经过点集为 1,当前到达第 0 个点 的最短路径初始化为 0

    for (int i = 0; i < 1 << n; i ++ )// 从 0 到 111...11 枚举所有 state
        for (int j = 0; j < n; j ++ )// 枚举所有 state 到达的点
            if (i >> j & 1) // 如果当前点集包含点 j,那么进行状态转移
                for (int k = 0; k < n; k ++ )// 枚举所有 k
                    if (i >> k & 1)
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);

    cout << f[(1 << n) - 1][n - 1];

    return 0;
}

没有上司的舞会
树形DP问题。
状态表示
f[u][0]:所有以u为根的子树中选择,并且不选u这个点的方案

f[u][1]:所有以u为根的子树中选择,并且选u这个点的方案

属性:Max

状态计算
当前u结点不选,子结点可选可不选

  • f [ u ] [ 0 ] = ∑ m a x ( f [ s i , 0 ] , f [ s i , 1 ] ) f [ u ] [ 0 ] = ∑ m a x ( f [ s i , 0 ] , f [ s i , 1 ] ) f[u][0]=∑max(f[si,0],f[si,1])f[u][0]=∑max(f[si,0],f[si,1]) f[u][0]=max(f[si,0],f[si,1])f[u][0]=max(f[si,0],f[si,1])

当前u结点选,子结点一定不能选

  • f [ u ] [ 1 ] = ∑ ( f [ s i , 0 ] ) f [ u ] [ 1 ] = ∑ ( f [ s i , 0 ] ) f[u][1]=∑(f[si,0])f[u][1]=∑(f[si,0]) f[u][1]=(f[si,0])f[u][1]=(f[si,0])

时间复杂度 O ( n ) O(n) O(n)

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

using namespace std;

const int N = 6010;

int n;
int h[N], e[N], ne[N], idx;
int happy[N];
int f[N][2];
bool has_fa[N];

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

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

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

    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(b, a);
        has_fa[a] = true;
    }

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

    dfs(root);

    printf("%d\n", max(f[root][0], f[root][1]));

    return 0;
}

不用建图的方法orz%%%

#include <iostream>
#include <cstdio>
using namespace std;
int dp[2][6010];//dp解释见上
int f[2][6010];//f[0]为父亲,f[1]为高兴值
int ind[6010];//入度
int vis[6010];//访问标记
int root;//树的根
void dfs(int u){//递归从后往前更新
    if(!u) return;
    vis[u]=1;//已访问
    root=u;//最后一个访问到的一定是根,所以一直更新根就行了
    dp[0][f[0][u]]+=max(dp[1][u]+f[1][u],dp[0][u]);//给父亲更新
    dp[1][f[0][u]]+=dp[0][u];
    ind[f[0][u]]--;//更新完一个子节点
    if(!ind[f[0][u]]) dfs(f[0][u]);//在所有子节点更新后再更新(入度为0)
}
int main(){
    int n=0;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&f[1][i]);
    int a,b;
    for(int i=1;i<n;i++){
        scanf("%d%d",&a,&b);
        f[0][a]=b;//保存节点信息
        ind[b]++;
    }
    for(int i=1;i<=n;i++)
        if(!vis[i]&&!ind[i])//没有被访问过,没有入度,说明是叶子节点
            dfs(i);
    printf("%d\n",max(dp[0][root],dp[1][root]+f[1][root]));//取根节点两种方案的最大值
    return 0;
}

滑雪
本来是一个dfs的过程,遍历所有的位置,找到从当前位置往下走的最大路径,再取最大值,可是这样做会有很多重复的位置被重新计算过,因此可以利用空间换时间的思想,把遍历过的位置往下走的路径的最大值进行记录,这就是记忆化搜索
在这里插入图片描述
注意:f[][]二维数组初始化的时候最好统一赋值为-1,如果不进行初始化直接用0判断,此题可以,可是如果遇到一些记忆化搜索的问题要求方案数的时候,初始化是0可能会导致个别情况计算出来的恰好结果是0时,却被认为未遍历过,因此统一赋值为-1就没错了.
时间复杂度 O ( n 2 ) O(n^2) O(n2)

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

using namespace std;

const int N = 310;

int n, m;
int g[N][N];
int f[N][N];//f[i][j]表示所有从(i, j)开始滑的路径

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};//上右下左

int dp(int x, int y)
{
    if (f[x][y] != -1) return f[x][y];

    f[x][y] = 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])
            f[x][y] = max(f[x][y], dp(a, b) + 1);
    }

    return f[x][y];
}

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

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

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

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值