acwing算法基础之动态规划--数位统计DP、状态压缩DP、树形DP和记忆化搜索

news2024/12/25 13:20:52

目录

  • 1 基础知识
  • 2 模板
  • 3 工程化

1 基础知识

暂无。。。

2 模板

暂无。。。

3 工程化

题目1:求a~b中数字0、数字1、…、数字9出现的次数。

思路:先计算1~a中每位数字出现的次数,然后计算1~b-1中每位数字出现的次数,两个相减即是最终答案。

那么,如何计算1~a中每位数字出现的次数呢?

首先,将a的每一位存入向量num中,例如a=1234567,那么num为,
在这里插入图片描述
考虑如下两个子问题,

  1. 1~a中数字0出现的次数。
  2. 1~a中数字5出现的次数。为啥选择数字5呢?因为1到9中的任意一个数都和5等价。

对于问题1:1~x中数字0出现的次数。

记num中有n位,从第0位不考虑,因为第0位不可能取到0,即数字首位不能为0,例如0123,这样的数字是不合法的,应该表示成123。所以i从1到n-1,即for (int i = 1; i < n; ++i)。考虑如下情况,

  1. 当num的第i位取0且其左侧部分不取L时,有多少个数?
    在这里插入图片描述
    左边部分的方案数为:1~L-1,共L-1个数。
    右边部分的方案数为:0~ 1 0 n − 1 − i − 1 10^{n-1-i}-1 10n1i1,共 1 0 n − 1 − i 10^{n-1-i} 10n1i个数。
    故总方案数为: ( L − 1 ) ⋅ 1 0 n − 1 − i (L-1)\cdot 10^{n-1-i} (L1)10n1i

  2. 当num的第i位取0且其左侧部分取L时,
    2.1 如果第i位原先数字等于0,那么左边方案数=1,右边方案数=0~R,共R+1个,总方案数=1 * (R+1) = R+1。
    2.2 如果第i位原先数字大于0,那么左边方案数=1,右边方案数=0~ 1 0 n − 1 − i − 1 10^{n-1-i}-1 10n1i1,共 1 0 n − 1 − i 10^{n-1-i} 10n1i个数。

对于问题2:1~x中数字5出现的次数。

如果num的第0位等于5,有R+1个方案数。如果num的第0位大于5,有 1 0 n − 1 10^{n-1} 10n1个数。

i从1到n-1,即for (int i = 1; i < n; ++i)。考虑如下情况,

  1. 当num的第i位取5且其左侧部分不取L时,有多少个数?
    在这里插入图片描述
    左边部分的方案数为:0~L-1,共L个数。
    右边部分的方案数为:0~ 1 0 n − 1 − i − 1 10^{n-1-i}-1 10n1i1,共 1 0 n − 1 − i 10^{n-1-i} 10n1i个数。
    故总方案数为: L ⋅ 1 0 n − 1 − i L\cdot 10^{n-1-i} L10n1i

  2. 当num的第i位取5且其左侧部分取L时,
    2.1 如果第i位原先数字等于5,那么左边方案数=1,右边方案数=0~R,共R+1个,总方案数=1 * (R+1) = R+1。
    2.2 如果第i位原先数字大于5,那么左边方案数=1,右边方案数=0~ 1 0 n − 1 − i − 1 10^{n-1-i}-1 10n1i1,共 1 0 n − 1 − i 10^{n-1-i} 10n1i个数。

综合上述,C++代码如下,

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

using namespace std;
int a, b;

int compute_R(const vector<int>& num, int i) {//计算第i位右侧数字大小
    int R = 0;
    for (int k = i + 1; k < num.size(); ++k) {
        R = R * 10 + num[k];
    }
    return R;
}

int compute_L(const vector<int>& num, int i) {//计算第i位左侧数字大小
    int L = 0;
    for (int k = 0; k < i; ++k) {
        L = L * 10 + num[k];
    }
    return L;
}

int compute_cnt(int a, int x) {//计算1~a中x出现的次数
    if (a <= 0) return 0;
    
    vector<int> num; //把a转化为num,高位在前
    int t = a;
    while (t) {
        num.emplace_back(t % 10);
        t /= 10;
    }
    reverse(num.begin(), num.end()); //保证高位在前
    
    int n = num.size(); //a总共有n位
    
    int res = 0; //存储1~a中x出现的次数
    
    //考虑第0位取x的情况
    if (x != 0) {
        if (num[0] == x) {
            //有R+1个方案
            int R = compute_R(num, 0);
            res += R + 1;
        } else if (num[0] > x) {
            //有10^(n-1)个方案数
            res += pow(10, n - 1);
        }
    }
    
    //考虑第i位取x的情况
    for (int i = 1; i < n; ++i) {
        //计算情况1中x出现的次数
        if (x == 0) {
            int L = compute_L(num, i);
            res += (L - 1) * pow(10, n - 1 - i);
        } else {
            int L = compute_L(num, i);
            res += L * pow(10, n - 1 - i);
        }
        
        //计算情况2中x出现的次数
        if (num[i] == x) {
            int R = compute_R(num, i);
            res += R + 1;
        } else if (num[i] > x) {
            res += pow(10, n - 1 - i);
        }
    }
    
    return res;//返回1~a中x出现的次数
}


int main() {
    
    while (cin >> a >> b, a || b) {
        if (a > b) swap(a, b); //保证b是大数
        
        for (int x = 0; x < 10; ++x) {
            int cnt1 = compute_cnt(b, x); //计算1~b中x出现的次数
            int cnt2 = compute_cnt(a - 1, x); //计算1~a中x出现的次数
            cout << cnt1 - cnt2 << " ";
        }
        cout << endl;
        
    }
    
    return 0;
}

题目2:在N * M的棋盘下,摆放1*2的矩形块,可以竖着摆,也可以横着摆,要求摆满棋盘,求有多少种摆放方案。

解题思路:状态压缩类DP。

规定横着摆放的1*2的矩形块,第一个小方格为起点方格,第二个小方格为终点方格,如下图所示,
在这里插入图片描述
f[i][j]状态定义:前i-1列已经摆放完毕,第i列中终点方格的状态为j,的方案数。

其中终点方格的状态j是指,从第0行到第n-1行,有终点网格的记作1,没有终点网格的记作0。比如下图中第1列的终点方格的状态是 ( 1001 ) 2 (1001)_2 (1001)2,即十进制中的9。

在这里插入图片描述
f[i][j]的状态转移:f[i-1][k],即前i-2列已经摆放完毕,第i-1列中终点网格的状态为k。k需要满足两个条件:

  1. 第i-1列中的终点网格和起点网格不能重合,其中终点网格的状态为k,起点网格的状态为j,也即k & j == 0
  2. 连续的空网格的数目不能是奇数,否则竖着摆放不下1*2的矩形块。即数k | j二进制表示中连续0的数目不能是奇数。

故,综合上述,状态转移方程为,
f [ i ] [ j ] = ∑ k f [ i − 1 ] [ k ] f[i][j] = \sum_kf[i-1][k] f[i][j]=kf[i1][k]

初始化f[0][0] = 1

最终答案f[M][0],也即第0列、第1列、…、第M-1列均摆放完毕,第M列中终点格子的状态为0的方案数。

C++代码如下,

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

using namespace std;

const int N = 12, M = 1 << N;
bool st[M];
vector<vector<int>> last(M);
long long f[N][M];
int n, m;

int main() {
    while (cin >> n >> m, n || m) {
        //n*m的棋盘
        
        //步骤1:把状态0~1<<n-1中满足连续0个数为偶数的状态标识出来
        memset(st, 0, sizeof st); //多组测试数据,所以要初始化
        for (int i = 0; i < 1 << n; ++i) {
            //数i的二进制表示中,连续0的个数是不是都是偶数
            bool flag = true;
            int cnt = 0; //连续0的个数
            for (int j = 0; j < n; ++j) {
                if (i >> j & 1) {//第j位为1,前面的连续0之和为cnt
                    if (cnt & 1) {//如果cnt是奇数
                        flag = false;
                        break;
                    }
                    cnt = 0;
                } else {
                    cnt += 1;
                }
            }
            if (cnt & 1) flag = false;//判断最后一个连续0的数目
            
            st[i] = flag; //true表示状态i中连续0的个数都是偶数。
            //cout << "i = " << i << ", st[i] = " << st[i] << endl;
        }
        
        //步骤2:f[i][j]和f[i-1][k],存储j满足要求的k
        for (int j = 0; j < 1 << n; ++j) {
            last[j].clear(); //多组测试数据,所以要清空
            for (int k = 0; k < 1 << n; ++k) {
                if ((k & j) == 0 && st[k | j]) {
                    last[j].emplace_back(k);
                }
            }
        }
        
        //步骤3:正式dp
        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 : last[j]) {
                    f[i][j] += f[i-1][k];
                }
            }
        }
        
        cout << f[m][0] << endl;
    }
    return 0;
}

题目3:给定n*n的矩阵,表示n个结点两两之间的距离,求从0号结点出发到达第n-1号结点,经过每一个结点一次的路径的最小距离。

解题思路:状态压缩DP。

状态定义,f[i][j],即:所有从第0号结点走到第j号结点,且走过的结点是i的路径,的最小值。

其中i的二进制表示中第0位为1表示,经过第0号结点,否则不经过第0号结点。
i的二进制表示中第1位为1表示,经过第1号结点,否则不经过第1号结点。
i的二进制表示中第2位为1表示,经过第2号结点,否则不经过第2号结点。
……
i的二进制表示中第n-1位为1表示,经过第n-1号结点,否则不经过第n-1号结点。

注意,只有满足(i & 1) == 1 && (i & (1 << j)) == (1 << j)时,状态f[i][j]才合法。

状态转移,考虑最后一步咋走的,即f[last][k] + d[k][j],其中i包含结点k时才合法,即(i & (1 << k)) == (1 << k),且last = i - (1 << j)

故,综合上述,状态转移方程为,
f [ i ] [ j ] = m i n k { f [ l a s t ] [ k ] + d [ k ] [ j ] } f[i][j] = \underset {k}{min} \{ f[last][k] + d[k][j] \} f[i][j]=kmin{f[last][k]+d[k][j]}
l a s t = i − ( 1 < < k ) last = i - (1 << k) last=i(1<<k)

初始化,f[1][0] = 0,其余初始化为正无穷大。

最终答案,f[(1 << n) - 1][n-1]

C++代码如下,

#include <iostream>
#include <cstring>

using namespace std;

const int N = 21, M = 1 << N;
int f[M][N];
int n;
int d[N][N];

int main() {
    cin >> n;
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            cin >> d[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) {
            //从0出发到达j,经过的结点是i
            if ((i & 1) == 1 && (i & (1 << j)) == (1 << j)) {
                //f[i][j]是合法的
                for (int k = 0; k < n; ++k) {
                    if ((i & (1 << k)) == (1 << k)) {
                        //i中有k
                        int last = i - (1 << j);
                        f[i][j] = min(f[i][j], f[last][k] + d[k][j]);
                    }
                }
            }
        }
    }
    
    cout << f[(1 << n) - 1][n-1] << endl;
    
    return 0;
}

题目4:没有上司的舞会。直接上司和员工不能同时选择,求最大值。

解题思路:树形DP,从根结点递归处理。

状态定义,有,

  1. f[i][0],所有以i为根的子树中选择,但不选择结点i的方案数中的最大值。
  2. f[i][1],所有以i为根的子树中选择,且选择结点i的方案数中的最大值。

状态转移,

f [ i ] [ 0 ] = ∑ j m a x { f [ j ] [ 0 ] , f [ j ] [ 1 ] } f[i][0] = \sum_j max\{ f[j][0], f[j][1] \} f[i][0]=jmax{f[j][0],f[j][1]}
其中结点j为结点i的子结点。
f [ i ] [ 1 ] = w [ i ] + ∑ j f [ j ] [ 0 ] f[i][1] = w[i] + \sum_j f[j][0] f[i][1]=w[i]+jf[j][0]
其中结点j为结点i的子结点。

初始化,二维数组初始化为0。

记根结点为start,那么最终答案res = max(f[start][0], f[start][1])

C++代码为,

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

using namespace std;

const int N = 6010;

int n, start;
bool has_father[N];
int happy[N];
int f[N][2];
vector<vector<int>> sons(N);

void dfs(int i) {
    f[i][1] = happy[i];
    
    for (auto j : sons[i]) {
        dfs(j);
        f[i][1] += f[j][0];
        f[i][0] += max(f[j][0], f[j][1]);
    }
    return;
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; ++i) cin >> happy[i];
    
    for (int i = 1; i < n; ++i) {
        int a, b;
        cin >> a >> b;
        has_father[a] = true;
        sons[b].emplace_back(a);
    }
    
    //找到根结点
    start = -1;
    for (int i = 1; i <= n; ++i) {
        if (has_father[i] == false) {
            start = i;
            break;
        }
    }
    
    dfs(start);
    
    cout << max(f[start][0], f[start][1]) << endl;
    
    return 0;
}

题目5:滑雪。n*m的网格,可以往上下左右方向尝试,只能从高处滑到低处。求路径最长值。

思路:记忆化搜索,状态先全部初始化为-1,然后递归每一个位置,如果f[i][j] != -1,则说明它被计算过了,直接返回。

状态表示,f[i][j]:从(i,j)开始滑的最长路径值。

状态转移,有:

  1. 如果能往上滑,即(i-1,j)在网格内且h[i][j] > h[i-1][j],有f[i-1][j] + 1
  2. 如果能往下滑,即(i+1,j)在网格内且h[i][j] > h[i+1][j],有f[i+1][j] + 1
  3. 如果能往左滑,即(i-1,j)在网格内且h[i][j] > h[i-1][j],有f[i-1][j] + 1
  4. 如果能往右滑,即(i+1,j)在网格内且h[i][j] > h[i+1][j],有f[i+1][j] + 1

初始化,所有状态均设置为-1。

答案,即所有状态的最大值。

C++代码如下,

#include <iostream>
#include <cstring>

using namespace std;

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

int dirs[4][2] = {{1,0}, {-1,0}, {0,1}, {0,-1}};

int dfs(int i, int j) {
    if (f[i][j] != -1) return f[i][j];
    
    f[i][j] = 1;
    for (int k = 0; k < 4; ++k) {
        int x = i + dirs[k][0], y = j + dirs[k][1];
        if (x >= 1 && x <= n && y >= 1 && y <= m && h[i][j] > h[x][y]) {
            f[i][j] = max(f[i][j], dfs(x, y) + 1);
        }
    }
    
    return f[i][j];
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            cin >> h[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, dfs(i, j));
        }
    }
    
    cout << res << endl;
    
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1277115.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

YOLOv8优化策略:SENetV2,squeeze和excitation全面升级,效果优于SENet | 2023年11月最新成果

🚀🚀🚀本文改进: SENetV2,squeeze和excitation全面升级,作为注意力机制引入到YOLOv8,放入不同网络位置实现涨点 🚀🚀🚀YOLOv8改进专栏:http://t.csdnimg.cn/hGhVK 学姐带你学习YOLOv8,从入门到创新,轻轻松松搞定科研; 1.SENetV2 论文:https://arxiv.org/…

java:springboot3集成swagger(springdoc-openapi-starter-webmvc-ui)

背景 网上集成 swagger 很多都是 Springfox 那个版本的&#xff0c;但是那个版本已经不更新了&#xff0c;springboot3 集成会报错 Typejavax.servlet.http.HttpServletRequest not present&#xff0c;我尝试了很多才知道现在用 Springdoc 了&#xff0c;今天我们来入门一下 …

2023-12-02 LeetCode每日一题(拼车)

2023-12-02每日一题 一、题目编号 1094. 拼车二、题目链接 点击跳转到题目位置 三、题目描述 车上最初有 capacity 个空座位。车 只能 向一个方向行驶&#xff08;也就是说&#xff0c;不允许掉头或改变方向&#xff09; 给定整数 capacity 和一个数组 trips , trip[i] …

国际语音群呼系统

随着海外电话营销的发展&#xff0c;越来越多的出海企业通过国际语音群呼系统打开出海营销之路。企业出海营销运营&#xff0c;选择一个安全、高效、便捷的国际语音群呼系统非常重要。 一、什么是国际语音群呼系统&#xff1f; 国际语音群呼是指通过语音的方式批量向海外用户传…

一进三出宿舍限电模块的改造升级

一进三出宿舍限电模块改造升级石家庄光大远通电气有限公司智能模块功能特点&#xff1a; 电能控制功能&#xff1a;可实施剩余电量管理&#xff0c;电量用完时将自动断电&#xff1b; 剩余电量可视报警提示功能&#xff1a;剩余电量可视&#xff0c;并当电量剩余5度时&#xff…

cpu版本的torch可以用清华镜像源安装

一、来到pytroch官网找到如下代码 官方提供的默认的安装cpu版本的torch的命令 pip3 install torch torchvision torchaudio二、使用清华镜像安装 pip3 install torch torchvision torchaudio -i https://pypi.tuna.tsinghua.edu.cn/simple

力扣题:字符串的反转-11.24

力扣题-11.24 [力扣刷题攻略] Re&#xff1a;从零开始的力扣刷题生活 力扣题1&#xff1a;151. 翻转字符串里的单词 解题思想&#xff1a;保存字符串中的单词即可 class Solution(object):def reverseWords(self, s):""":type s: str:rtype: str"&quo…

项目实战-编写ssm整合配置文件

1、父工程pom.xml <properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><spring.version>…

继承 和 多肽(超重点 ! ! !)

[本节目标] 1.继承 2.组合 3.多肽 1.继承 1.1 为什么要继承 Java中使用类对现实世界中实体来进行描述&#xff0c;类经过实例化之后的产物对象&#xff0c;则可以用来表示现实中的实体&#xff0c;但是现实世界错综复杂&#xff0c;事物之间可能会存在一些关联&#xff0…

计算机组成学习-计算机系统概述总结

1、计算机系统概述 日常见到的计算机有显示器、键盘、鼠标、音箱、主机箱等&#xff1b;主机箱中有&#xff1a;主板、CPU、硬盘、内存、显卡、声卡等&#xff1b; 1.1 计算机系统层次结构 1.2 计算机系统的基本组成 包括硬件系统和软件系统两部分。 1.2.1 计算机硬件 计算…

HarmonyOS 开发案例分享:万能卡片也能用来玩游戏

一、前言 作为一名开发爱好者&#xff0c;从大了讲&#xff0c;我学习并进行 HarmonyOS 相关开发是为了能为鸿蒙生态建设尽一份绵薄之力&#xff0c;从小了讲&#xff0c;就是为了自己的兴趣。而万能卡片是一个让我非常感兴趣的东西。 很多时候我跟别人解释什么是万能卡片&…

LLM:《第 3 部分》从数学角度评估封闭式LLM的泛化能力

一、说明 在 OpenAI 或 Anthropic 等封闭式大型语言模型 (LLM) 领域&#xff0c;对智能和多功能性的真正考验在于它们处理高特异性查询并在响应中表现出独特性的能力。在这篇博客中&#xff0c;我的目标是提供测试这些模型泛化能力的机制。 封闭式LLM意味着您不知道训练语料库的…

matlab操作方法(二)——基本作图

matlab提供很多灵活的二维作图功能函数。这些作图函数分为3类&#xff1a;图形处理、曲线和曲面图的创建、注释和图形特性。作图函数虽多&#xff0c;但语法大致相同 在 MATLAB 中&#xff0c;figure 函数用于创建或选择图形窗口。 matlab figure函数的用法_matlab中figure-C…

【数据结构】环形队列

环形队列 1. 定义 环形队列就是将队列在逻辑上看作环形结构、物理上仍是数组形式存储的一种数据结构。 其实现主要分为两种情况&#xff1a; 浪费空间法记录空间法 2. 实现 实现要考虑的是成员变量 2.1 记录空间法 使用used标识当前存储了多少元素&#xff0c;如果为空&a…

数据结构(三)——算法和算法分析

&#x1f600;前言 数据结构和算法是计算机科学领域中至关重要的概念。它们为解决实际问题提供了有效的方法和步骤。算法作为解决问题的方法和步骤&#xff0c;在计算机中以指令的有限序列的形式表达。本文将介绍算法的定义、描述和程序设计等方面的内容&#xff0c;帮助您深入…

Hdoop学习笔记(HDP)-Part.13 安装Ranger

目录 Part.01 关于HDP Part.02 核心组件原理 Part.03 资源规划 Part.04 基础环境配置 Part.05 Yum源配置 Part.06 安装OracleJDK Part.07 安装MySQL Part.08 部署Ambari集群 Part.09 安装OpenLDAP Part.10 创建集群 Part.11 安装Kerberos Part.12 安装HDFS Part.13 安装Ranger …

【Linux】信号概念和信号的产生

文章目录 一、什么是信号&#xff1f;1.signal系统调用2.从硬件解析键盘数据如何输入给内核3.同步和异步 二、信号的产生1.键盘组合键2. kill命令3.系统调用接口3.1kill3.2 raise3.3abort 4.异常5.软件条件 重谈core dump标志位 一、什么是信号&#xff1f; 以日常为例&#x…

边缘数据中心和5G的融合彻底改变数据传输和物联网

伴随着数字化时代的飞速发展&#xff0c;边缘数据中心和5G技术的联袂崛起&#xff0c;正深刻塑造着人们对数据的创造、传输和处理方式。据Gartner公司的预测&#xff0c;到2025年&#xff0c;企业数据的三分之二将在边缘计算设施中涌现&#xff0c;而非传统的集中式数据中心。这…

leetcode 209. 长度最小的子数组(优质解法)

代码&#xff1a; //时间复杂度 O(N) ,空间复杂度 O(1) class Solution {//采用滑动窗口的方法解决public int minSubArrayLen(int target, int[] nums) {int numsLengthnums.length;int minLengthInteger.MAX_VALUE;int left0;int right0;int sum0;while (right<numsLengt…

全栈冲刺 之 一天速成MySQL

一、为什么使用数据库 数据储存在哪里&#xff1f; 硬盘、网盘、U盘、光盘、内存&#xff08;临时存储&#xff09; 数据持久化 使用文件来进行存储&#xff0c;数据库也是一种文件&#xff0c;像excel &#xff0c;xml 这些都可以进行数据的存储&#xff0c;但大量数据操作…