数位DP套路模板

数位DP

【介绍】

显然,是一种DP。

再显然,是一种关于每一位数字之间关系(性质)的DP


【题目特征】

题目描述一般是求【L,R】范围内满足各位数字间存在某种规律的数有几个

数位DP的题目数据范围往往很大,动不动就是1e18,但是由于是数位DP只跟位数有关,所以也就18位。


【算法设计】

使用记忆化搜索进行DP因为会出现重复出现的状态,使用记忆化搜索可以减少重复的搜索,减少复杂度。

一.记忆化搜索的过程

​ 从起点向下搜索,搜索的途中累加每一位的答案,最后在起点得到答案。

二. 区间转化为[0,X]

​ 对于[L,R]的区间问题,一般我们可以根据前缀和相减转化为[0,R] - [0,L-1]的问题。

三.dfs的参数

pos, 当前找到了第几位
limit,因为问题已经转化为了小于X的所有正整数中符合条件的个数,我们从高位枚举,一旦当前位小于X,那么后面数字可以随便选。limit就是判断当前位是否可以任意选。

递归中limit的传递

1.当前没有限制limit=0,那么显然后面应该都没限制,是0

2.当前有限制limit=1;

​ 1.如果当前位选的数小于X对应位数字,那么后面应该是没限制0

​ 2.如果当前位选的数恰好等于X对应位数字,这个分支相当于是紧贴着X的每一位(前面枚举到的位选的和X一样),那么后面还要限制数字大小,limit为1

综上:我们可以总结出limit的转移方程 limit = limit&&j==X[i] (X[i]是当前位能枚举得最大值, j是当前正在枚举得数)

3.**pre,**有些题目的性质是和前几位数字有关,那么也可以加上pre1…pre2

4.**zero,**判断是否有前导0,比如所有位相同的数包括000333(实质是333)前面都是0的话取1,否则取0

递归中zero前导零的传递

1.前一位是前导0

​ 1.当前位是0,那么后面还是zero还是1

​ 2.当前位不是0,那么后面zero=0

2.前一位不是前导0,那么不管当前位是不是0,后面zero都是1

综上:我们可以总结出zero的转移方程 zero = zero&&(!j) (X[i]是当前位能枚举得最大值, j是当前正在枚举得数)

四.记忆化搜索的记忆化

​ 我们可以用一个dp数组来记录已经确认的状态的值,下标用来表示状态,等到后面搜到相同状态时不用递归到最底层直接可以拿来复用。

五.模板
ll dfs(int pos,int pre,int st,……,int lead,int limit)//记忆化搜索
{
    if(pos>len) return st;//剪枝
    if((dp[pos][pre][st]……[……]!=-1&&(!limit)&&(!lead))) return dp[pos][pre][st]……[……];//相同状态,也可以吧limit和lead也放入dp数组中(多开两个维度)
    ll ret=0;//暂时记录当前方案数
    int res=limit?a[len-pos+1]:9;//res当前位能取到的最大值,limit的作用
    for(int i=0;i<=res;i++)//搜索每个可取的数
    {
        //有前导0并且当前位也是前导0
        if((!i)&&lead) ret+=dfs(……,……,……,i==res&&limit);
        //有前导0但当前位不是前导0,当前位就是最高位
        else if(i&&lead) ret+=dfs(……,……,……,i==res&&limit); 
        else if(根据题意而定的判断) ret+=dfs(……,……,……,i==res&&limit);
    }
    if(!limit&&!lead) dp[pos][pre][st]……[……]=ret;//没前导零,后面无限制,当前状态方案数记录
    return ret;
}
ll part(ll x)//把数按位拆分
{
    len=0;
    while(x) a[++len]=x%10,x/=10;
    memset(dp,-1,sizeof dp);//初始化-1(因为有可能某些情况下的方案数是0)
    return dfs(……,……,……,……);//进入记搜
}
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%lld%lld",&l,&r);
        if(l) printf("%lld",part(r)-part(l-1));//[l,r](l!=0)
        else printf("%lld",part(r)-part(l));//从0开始要特判
    }
    return 0;
}

【例题】

一般的数位DP套路题都是一个数满足什么性质,而这题是两个数满足一个性质就是与起来是0。那么我们类比一下,原来限制单个数后面取值的limit现在要有两个,而当前搜索的位置因为两个数位置总是同步的,所以只需要一个。因为两个数&起来等于0,那么他们相加是不进位的,又取了log,答案就是最高位的位置加1,所以每一位的贡献就是这位是最高位的数量乘以这位的位置+1.

#include<bits/stdc++.h>
using namespace std;


#define mem(a,b) memset(a,b,sizeof a)
#define PII pair<int,int>
#define ll long long
#define ull unsigned long long
#define fi first
#define se second
#define endl '\n'
#define PI acos(-1.0)
#define lcm(a,b) a/gcd(a,b)*b
#define INF 0x3f3f3f3f3f3f3f3f
#define debug(a) cerr<<#a<<"="<<a<<endl;
#define Adebug(a,i) cerr<<#a<<"["<<i<<"]="<<a[i]<<endl;
#define int long long
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define vi vector<int>
#define vpii vector<PII>
#define pb push_back
#define rvs(s) reverse(s.begin(),s.end())
#define all(s) s.begin(),s.end()
#define sz(s) (int)(s.size())
#define lb(s) ((s) & (-s))
#define mk(s, t) make_pair(s, t)


inline void wt(ll x){cout << x;}
inline void wtl(ll x){cout << x << endl;}
inline void wtb(ll x){cout << x << ' ';}
template <typename T> bool ckmax(T &x, T y){return x < y ? x = y, true : false;}
template <typename T> bool ckmin(T &x, T y){return x > y ? x = y, true : false;}
int qmi(int a, int k, int p){int res = 1;while (k){if (k & 1) res = (ll)res * a % p;a = (ll)a * a % p;k >>= 1;}return res;}
int qpow(int a,int b){int res = 1;while(b){if(b&1) res *= a;b>>=1;a*=a;}return res;}
int mo(int x,int p){return x = ((x%p)+p)%p;}
int gcd(int a,int b){return b?gcd(b,a%b):a;}
const int maxn = 1e6+7;
const int mod = 1e9+7;
int dx[] = {0,0,1,-1}, dy[] = {1,-1,0,0};

int T,N,M,K;
int num1[33],num2[33],dp[33][2][2];
int st[maxn];
int bit[33];
int L, R, ans;

int dfs(int now, int limit1, int limit2, int zero){
	if(now == -1) return 1;//枚举完了
	int &x = dp[now][limit1][limit2];
	if(~x) return x;//已经搜过了
	if ((!limit1)&&(!limit2)&&!zero) return x = bit[now+1];//没有限制,后面就是全部的组合
	int end1 = limit1?num1[now]:1;
	int end2 = limit2?num2[now]:1;
	x = 0;
	int temp = 0, cnt = 0;
	rep(i,0,end1){
		rep(j,0,end2){
			if(i&j) continue;//不符合&=0
			if(zero&&(i|j)){
				temp = dfs(now-1,limit1&&(i==end1),limit2&&(j==end2),0);
				x = (x+temp)%mod;//为上一层累加
				cnt = (cnt+temp)%mod;//当前位是最高位
			}
			else{
				x = (x+dfs(now-1,limit1&&(i==end1),limit2&&(j==end2),zero&&(!(i|j))))%mod;//为上一层累加
			}
		}
	}
	ans=(ans+cnt*(now+1))%mod;
	return x;
}
void part(int X, int Y){
	int len = -1;//数字放在【0,len】中
	while(X || Y) {
		num1[++len] = X%2, X/=2;
		num2[len] = Y%2, Y/=2;
	}
	ans = 0;
	mem(dp,-1);
	dfs(len,1,1,1);
	wtl(ans);
}
void solve(){
	cin >> L >> R;
	part(L,R);

}
signed main()
{

	bit[0] = 1;
	rep(i,1,32) bit[i] = bit[i-1]*3%mod;
	cin >> T;
	while(T--) solve();
  return (0-0); //<3
} 

这题与上题不同,只有一个数,我们可以用前缀和相减来求,但是数字太大有5000位,需要用到字符串来存储,对字符串求减一涉及到借位,可能有些麻烦(其实也并不麻烦)。这里提供另一种思路,就是判断L这个数是否满足即可。

参数分析:

  1. 满足f(x)= x(mod M) 我们可以构造g(x)= f(x)- x,那么条件变成g(x)= 0 (mod M)
  2. 计算f(x)时,为了避免重复计算贡献,规定当前数字只会跟前面的数字产生贡献,那么我们只需要记录每一位数字的前缀和pre,然后当前数字的贡献就是j * pre
#include<bits/stdc++.h>
using namespace std;


#define mem(a,b) memset(a,b,sizeof a)
#define PII pair<int,int>
#define ull unsigned long long
#define fi first
#define se second
#define endl '\n'
#define PI acos(-1.0)
#define lcm(a,b) a/gcd(a,b)*b
#define INF 0x3f3f3f3f3f3f3f3f
#define debug(a) cerr<<#a<<"="<<a<<endl;
#define Adebug(a,i) cerr<<#a<<"["<<i<<"]="<<a[i]<<endl;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define vi vector<int>
#define vpii vector<PII>
#define pb push_back
#define rvs(s) reverse(s.begin(),s.end())
#define all(s) s.begin(),s.end()
#define sz(s) (int)(s.size())
#define lb(s) ((s) & (-s))
#define mk(s, t) make_pair(s, t)


inline void wt(int x){cout << x;}
inline void wtl(int x){cout << x << endl;}
inline void wtb(int x){cout << x << ' ';}
template <typename T> bool ckmax(T &x, T y){return x < y ? x = y, true : false;}
template <typename T> bool ckmin(T &x, T y){return x > y ? x = y, true : false;}
int qpow(int a,int b){int res = 1;while(b){if(b&1) res *= a;b>>=1;a*=a;}return res;}
int mo(int x,int p){return x = ((x%p)+p)%p;}
int gcd(int a,int b){return b?gcd(b,a%b):a;}
const int maxn = 1e6+7;
const int mod = 1e9+7;

int T,N,M,K;
int bit[5009],A[5009],dp[5009][61][61][2],id,st[5009][61][61][2];
char R[5009],L[5009];
int f;
int dfs(int now,int pre,int fx, int limit){
	long long ans = 0;
	if(now==(N+1)){
		f = (fx==0);//判断X是否可行
		return f;
	}
	if (st[now][pre][fx][limit]==id) return dp[now][pre][fx][limit];//这样可以不用初始化dp数组,等于当前id说明是本组数据,不同组的模数是不同的,所以同一个状态的值可能不同
	st[now][pre][fx][limit] = id;
	int lim = limit?A[now]:9;
	rep(i,0,lim){
		int dif = mo((fx+i*pre-i*bit[N-now]),M);
		ans = ans+dfs(now+1,(pre+i)%M,dif,limit&&(i==A[now]));
	}
	ans = (ans+mod)%mod;
	dp[now][pre][fx][limit] = ans;
	return ans;
}
int part(char X[]){
	//printf("!---%s\n",X+1);
	++id;//标记测试数据组别
	N = strlen(X+1);	
	rep(i,1,N) A[i] = X[i]-'0';
	return dfs(1,0,0,1);	
}
void solve(){
	scanf("%s %s %d",L+1,R+1,&M);
	N = strlen(R+1);
    bit[0] = 1;
	rep(i,1,N){
		bit[i] = bit[i-1]*10%M;
	}
	int ans = part(R);
	f = 0;
	ans-=part(L);
	//debug(f);
	ans+=f;//f是判断L是否有效,有效的话就为1(前缀和相减时多减了1加回来)
	wtl(mo(ans,mod));

}
signed main()
{
	cin >> T;
	while(T--) solve();
    return (0-0); //<3
} 
### LeetCode Top 100 题目模式与解决方法 LeetCode 的 Top 100 经典题目涵盖了多种算法据结构的核心概念。以下是这些题目中的常见模式以及对应的解决方案。 #### 据结构分类 1. **组** 组类问题是基础,通常涉及双指针、滑动窗口等技巧。 - 双指针用于处理有序组或寻找特定条件下的两个[^1]。 ```python def two_sum(nums, target): left, right = 0, len(nums) - 1 while left < right: current_sum = nums[left] + nums[right] if current_sum == target: return [left, right] elif current_sum < target: left += 1 else: right -= 1 return [] ``` 2. **链表** 链表操作常涉及到反转、删除节点等问题。 - 使用虚拟头结点简化边界情况的处理[^2]。 ```python class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_list(head): prev = None curr = head while curr is not None: temp = curr.next curr.next = prev prev = curr curr = temp return prev ``` 3. **栈与队列** 这些据结构适用于括号匹配、BFS DFS 等场景。 - BFS 常见于图遍历或者最短路径问题[^3]。 ```python from collections import deque def bfs(graph, start_node): visited = set() queue = deque([start_node]) while queue: node = queue.popleft() if node not in visited: visited.add(node) for neighbor in graph[node]: if neighbor not in visited: queue.append(neighbor) return list(visited) ``` 4. **树** 树形结构主要考察二叉搜索树 (BST) 特性递归实现。 - 中序遍历可以验证 BST 是否合法[^4]。 ```python def is_valid_bst(root): stack, inorder = [], float('-inf') while stack or root: while root: stack.append(root) root = root.left root = stack.pop() if root.val <= inorder: return False inorder = root.val root = root.right return True ``` 5. **动态规划** 动态规划通过子问题分解来优化复杂度。 - 背包问题是一个典型的例子[^5]。 ```python def knapsack(weights, values, capacity): dp = [[0]*(capacity+1) for _ in range(len(values)+1)] for i in range(1, len(values)+1): for w in range(capacity+1): if weights[i-1] <= w: dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1]) else: dp[i][w] = dp[i-1][w] return dp[-1][-1] ``` 6. **回溯法** 回溯法适合组合、排列等相关问题。 - N皇后问题展示了如何利用剪枝减少不必要的计算[^6]。 ```python def solve_n_queens(n): result = [] cols = set(); diag1 = set(); diag2 = set() def backtrack(row, path): if row == n: result.append(path[:]) return for col in range(n): d1 = row - col; d2 = row + col if col in cols or d1 in diag1 or d2 in diag2: continue cols.add(col); diag1.add(d1); diag2.add(d2) path.append('.'*col + 'Q' + '.'*(n-col-1)) backtrack(row+1, path) cols.remove(col); diag1.remove(d1); diag2.remove(d2) path.pop() backtrack(0, []) return result ``` 7. **贪心算法** 贪心策略在某些情况下能够快速找到全局最优解。 - 区间调度是最常见的应用之一[^7]。 ```python def interval_scheduling(intervals): intervals.sort(key=lambda x: x[1]) # Sort by end time. count = 0; last_end_time = float('-inf') for start, end in intervals: if start >= last_end_time: count += 1 last_end_time = end return count ``` 8. **位运算** 位运算是高效解决问题的一种方式。 - 找到只出现一次的字可以通过异或完成[^8]。 ```python def single_number(nums): res = 0 for num in nums: res ^= num return res ``` 9. **字符串** 字符串处理经常需要用到 KMP 或者 Manacher 算法。 - 判断回文串可以用中心扩展法[^9]。 ```python def longest_palindrome(s): if not s: return "" def expand_around_center(left, right): while left >= 0 and right < len(s) and s[left]==s[right]: left -= 1; right += 1 return s[left+1:right] longest = "" for i in range(len(s)): odd = expand_around_center(i,i) even = expand_around_center(i,i+1) longer = odd if len(odd)>len(even) else even if len(longer) > len(longest): longest = longer return longest ``` #### 总结 以上列举了一些经典的 LeetCode 解决方案及其背后的理论依据。每种类型的题目都有其独特的解思路,掌握这些通用模板有助于提高刷题效率并加深理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值