【算法竞赛】数位dp(超详细版,新手易懂)

数位dp是一个在竞赛中非常常用的思想。本文将从一道经典的竞赛题目出发,详解数位dp的思路及相关知识点的用法,非常适合初学者,简单易懂,知识点涵盖全面。

目录

题目

预备知识1——判断互质

预备知识2 ——二进制位运算存储状态

思路详解

代码

运行结果


题目

 8.回路计数 - 蓝桥云课

预备知识1——判断互质

两个数互质即两个数的最大公约数为1

1.如果是c++17及以上版本,可以直接用内置函数gcd(a,b)判断 

#include <algorithm>
if(gcd(x,y)==1) flag=true;

2.如果版本不够,可以手写gcd,用辗转相除法实现

(1)原理

可以由数学推导得到,这里不作过多解释

(2)代码实现

//辗转相除法计算最大公约数
int gcd(int a, int b) {
    while (b != 0) {
        int temp = a % b;
        a = b;
        b = temp;
    }
    return a;
}

用递归实现也可以

3.注意

(1)1和任何正整数互质

(2)除了1以外,一个数和它本身的最大公约数等于它本身,因此一个数和它本身不互质

 

预备知识2 ——二进制位运算存储状态

1.原理

使用一个整数的每一位来存储一个布尔状态,整数的每一位可以表示一个状态

2.操作

  • 设置某一位:x |= (1 << pos) x 的第 pos 位设置为 1【即1后面有pos个0,因为dp是从第0为开始的】
  • 清除某一位:x &= ~(1 << pos)x 的第 pos 位设置为 0
  • 切换某一位:x ^= (1 << pos)x 的第 pos 位取反
  • 获取某一位的值:(x >> pos) & 1 获取 x 的第 pos 位的值

思路详解

一眼望去是一个非常清晰的搜索题,保证结点不重复+访问全部结点,但是用搜索做的话跑不出来答案,因为时间复杂度太高了,最多可以达到21!

搜索题用暴力做不出来,一般都是考虑用dp优化

1. 如何设置dp数组?

首先,很容易看出来这道题是一个典型的不同状态对应不同的访问方案(即如何来的),所以可以用dp。进一步思考,当前状态由哪些属性构成?
(1)当前所在点的位置

(2)当前已访问的状态

【tips:对于路径行进问题的dp,基本都是由这两个属性构成:当前点+已访问】

所以,设置dp[i][j]

  • i:当前访问到第i个结点
  • j:二进制,000...1..000,为1的数位表示已经访问过的

【j的空间最小要保证2的21次方,因为至少有21位】

2. 如何设置状态转移方程?

其实这道题就可以看作爬楼梯的变体版,当前新增状态和之前已经“爬过的”状态有关,但是它不像爬楼梯那样只和前一次或两次的状态有关,状态的位置是多变的

状态的位置是多变的,所以这也是这道题考虑状态压缩(数位dp)的原因

但其实本质是一样的,只用看当前点的当前状态可以由哪些上一步点的上一步状态得到,就+=上一步的方案数就可以了

3.状态转移核心流程是什么样的?

for(all 当前状态j)

        for(all 当前节点i)

                if(当前状态不包括当前点)continue;//说明当前状态和当前点无关

                for(当前结点i的所有邻接点k)

                {

                        if(非邻接) continue;

                        t=可能的上一步状态;//当前状态j去掉当前结点i得到

                        if(上一步状态不包含上一步邻接点k) continue;

                        dp[i][j]+=dp[k][t];

                }

4.当前状态j的边界条件是什么?

所有结点均已访问

j<(1<<21):即j的max值为1<<21-1

注意:1<<21是1向左边移动了21位,即1的后面有21个0(而不是1位于第21位上,后面只有20个0),所以1<<21的结果是1000000000000000000000,则(1 << 21) - 1的结果是0111111111111111111111,即 21 个 1

代码

#include <iostream>
typedef long long ll;//使用typedef定义
//或者写作#define int long lon,把long long类型写作int
//#define int long long
using namespace std;
int mp[30][30]={0};
const int M=25;
const int N=1<<25;
ll dp[M][N];
//爬楼梯的变体版,当前新增状态和之前已经“爬过的”状态有关
//不像爬楼梯那样只和前一次或两次的状态有关
//状态的位置是多变的,考虑状态压缩
//dp[i][j...]:i:当前访问到i号楼,j:二进制表示访问状态 

//gcd
int gcd(int a,int b)
{
	while(b!=0)
	{
		int tmp=a%b;
		a=b;
		b=tmp;
    }
    return a;
 } 
 int main()
 {
 	for(int i=1;i<=21;i++)
 	{
 		for(int j=1;j<i;j++)
			if(gcd(i,j)==1)mp[i][j]=mp[j][i]=1;
	}
	//初始化起点状态
	dp[1][1]=1; 
	//遍历所有状态 
	for(int j=1;j<(1<<21);j++)
	{
		//遍历所有当前点
		for(int i=1;i<=21;i++)
		{
			//当前状态不包含当前点
			if(((j>>(i-1))&1)==0) continue;
			//遍历当前点的所有邻接点
			//看可以从哪些状态得到当前状态
			for(int k=1;k<=21;k++)
			{
				//不邻接 
				if(mp[i][k]==0) continue;
				//邻接
				//上一个状态 
				int t=j-(1<<(i-1));
				//如果上一个状态不包含点k(易漏判)
				if(((t>>(k-1))&1)==0) continue;
				//可以由上一个节点的上一个状态得来 
				dp[i][j]+=dp[k][t];
			} 
		} 
	}
	//1和所有数互质
	//肯定可以重新回到起点
	ll ans=0;
	for(int i=1;i<=21;i++) ans+=dp[i][(1<<21)-1];
	cout<<ans;
	return 0; 
 }

1.注意1:移动时的数位问题

判断第i位时应该是移动i-1位,因为位是从0开始的

j<(1<<21)

2.注意2:最后的答案为什么是这样来的

ll ans=0;
for(int i=1;i<=21;i++) ans+=dp[i][(1<<21)-1];
cout<<ans;

因为答案成立需要满足两个条件:
(1)所有点都已经访问到

(2)最终回到起点

(1<<21)-1表示所有的状态都已经访问到了,只差回到起点就可以了

而1和所有数互质,所以和所有节点都有通路,肯定可以重新回到起点

3.注意3:数据类型重新定义问题

(1)方法一:typedef

typedef long long ll;//使用typedef定义

注意:别忘了;

(2)方法二:define

#define ll long long

 注意:①没有;②重新定义后的名字是写在前面

还有一种常见的用法就是, #define int long long,因为我们一般都是写int比较顺手,索性就把int全都改成long long,但非常不建议这么做,一是本来没有必要用long long的全都用了longlong导致空间的浪费,严重的情况下还会导致报错,二是如果这样用了函数的主入口就会被识别成long long main(),会导致编译错误,主入口只能用signed main()代替int main()

4.注意4:运算符优先级问题

对于表达式 ((j>>(i-1))&1)==0,如果不加括号,默认的优先级是什么样的呢?

  1. 位移运算符 >>:优先级较高,先于按位与运算符 & 和比较运算符 == 执行。

  2. 按位与运算符 &:优先级次之,先于比较运算符 == 执行。

  3. 比较运算符 ==:优先级最低,最后执行

但是没有必要刻意去记运算符的优先级,很容易出错,所以在写代码时我们统一加上括号

运行结果

### Python算法练习题目资源教程 Python 是一种功能强大且灵活的语言,非常适合用于解决各种算法问题。以下是关于 Python 算法练习的一些推荐资源和经典题目。 #### 1. 分治算法练习 分治算法是一种通过将复杂问题分解成更小子问题来解决问题的方法。以下是一个经典的分治算法例子: ```python def mypow(b, p, k): if p == 0: # 递归结束条件 return 1 t = mypow(b, p // 2, k) % k # 对半递归计算幂次方并取模 x = (t * t) % k # 计算平方后的结果并对k取余 if p % 2 != 0: # 如果指数是奇数,则额外乘一次底数 x = (x * b) % k return x ``` 上述代码展示了如何使用递归来高效计算大数的幂次方,并通过取模减少中间结果大小[^1]。 --- #### 2. 枚举算法练习 枚举算法通常用来穷尽所有可能的情况以找到最优解或满足特定条件的结果。下面是一个简单的枚举算法实例: ```python n = int(input()) ans = 0 for i in range(n): # 遍历从0到n-1的所有整数 if '2' not in str(i): # 判断当前数字是否不包含字符‘2’ ans += 1 print(ans) ``` 这段代码实现了统计给定范围内不含数字 ‘2’ 的正整数数量的功能[^2]。 --- #### 3. 贪心算法练习 贪心算法的核心在于每一步都做出局部最优的选择,从而期望达到全局最优解。例如,在分配饼干给孩子的问题中,可以通过排序数组的方式简化逻辑: ```python def findContentChildren(greedy, size): greedy.sort() # 排序孩子的胃口需求 size.sort() # 排序饼干尺寸 child_index, cookie_index = 0, 0 while child_index < len(greedy) and cookie_index < len(size): if size[cookie_index] >= greedy[child_index]: child_index += 1 cookie_index += 1 return child_index ``` 此函数返回能够被满足的孩子数目[^4]。 --- #### 4. 动态规划与回溯算法 动态规划适合于具有重叠子结构的问题,而回溯法则常应用于组合搜索空间较大的场景。例如,斐波那契序列可以用动态规划有效求解: ```python def fibonacci(n): dp = [0] * (n + 1) dp[0], dp[1] = 0, 1 for i in range(2, n + 1): dp[i] = dp[i - 1] + dp[i - 2] return dp[n] ``` 以上代码展示了一个基于动态规划思想的经典 Fibonacci 数列实现方式。 --- #### 5. 实际应用案例:抓交通肇事犯 假设我们需要找出某个四位车牌号码对应的年份,可以采用双重循环策略完成任务: ```python for year in range(1970, 2001): # 外层循环遍历年份范围 k = year % 10000 # 提取最后四位作为候选车牌号 flag = False for temp in range(31, 100): # 内层循环验证是否存在符合条件的temp值 if temp * temp == k: print("车牌号为:", k) flag = True break if flag: continue ``` 这种方法通过对潜在数据进行筛选逐步缩小查找范围[^3]。 --- ### 总结 无论是基础还是高级别的算法训练,都可以借助 Python 来实践。它不仅语法简洁易懂,而且拥有丰富的库支持开发人员快速构建原型解决方案。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值