数位dp学习笔记(待续)

感觉这东西还是挺玄学的
个人理解是靠状态数来控制复杂度

CF55D Beautiful numbers

题意:求区间中能被数位上每个数字整除的数的个数
l , r &lt; = 1 0 18 l, r &lt;= 10 ^ {18} l,r<=1018
做法:数位dp经典问题
注意到 l c m ( 1 , 2 , . . . , 9 ) = 2520 lcm(1, 2,..., 9) = 2520 lcm(1,2,...,9)=2520, 所以途中只需记录模 2520 2520 2520所得的余数即可
d p [ p o s ] [ m o d ] [ l i m i t ] dp[pos][mod][limit] dp[pos][mod][limit]表示第pos位到末尾 前pos位模 2520 2520 2520所得余数为 m o d mod mod 当前出现数字的 l c m lcm lcm l i m i t limit limit的方案数
这题中学到一个小技巧,虽然在dp的时候需要考虑前面是否已经有一位比限制要小,但是状态中不需要记录这一维。原本以为记忆状态多是以空间换时间,但其实不记录这一维使多次询问之间的信息可以共享(现在想想如果记录这一维记忆化也没什么意义了)。数位dp时要注意,不是记忆的越具体越好,一定要让状态平凡到能让更多的询问共用。
虽然对于每个limit直接维护会使空间太大,但是其实可能成为limit的数只有50个左右,可以离散化处理。
复杂度: m a x ( O ( n ) ) = d i g i t [ n ] ∗ 2520 ∗ 50 max(O(n)) = digit[n] * 2520 * 50 max(O(n))=digit[n]252050
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std; 

ll dp[21][3001][51], be, en, b[51], pre[3001]; 
int gcd(int x, int y)
{
	return (y == 0) ? x : gcd(y, x % y); 
}
void init()
{
	int cnt = 0; 
	for(int i = 1; i <= 2520; i++)
		if(2520 % i == 0)pre[i] = ++cnt; 
}
ll solve(int p, int mod, int limit, int ok)
{
	//printf("%d %d %d %d %d\n", p, mod, limit, ok, zero); 
	if(~dp[p][mod][pre[limit]] && ok)return dp[p][mod][pre[limit]]; 
	if(p == 0)return (mod % limit == 0); 
	
	ll ans = 0; 
	if(ok)
	{
		for(int i = 0; i <= 9; i++)
		{
			int nl, nok; 
			nok = 1; 
			if(i != 0)nl = limit * i / gcd(limit, i); else nl = limit; 
			ans += solve(p - 1, (mod * 10 + i) % 2520, nl, nok); 
		}
	}
	else
	{
		for(int i = 0; i <= b[p]; i++)
		{
			int nl, nok; 
			if(i != b[p])nok = 1; else nok = 0; 
			if(i != 0)nl = limit * i / gcd(limit, i); else nl = limit; 
			ans += solve(p - 1, (mod * 10 + i) % 2520, nl, nok); 
		}
	}
	if(ok)dp[p][mod][pre[limit]] = ans; 
	return ans; 
}
int getlen(ll x)
{
	int j = 0; 
	while(x)
	{
		j++; 
		b[j] = x % 10; 
		x /= 10; 
	}
	return j; 
}
int main()
{
    int t; 
    scanf("%d", &t); 
	init(); 
	memset(dp, -1, sizeof(dp)); 
    while(t--)
    {
    	scanf("%I64d%I64d", &be, &en); 
    	int l = getlen(en); 
    	ll ans = solve(l, 0, 1, 0); 
    	l = getlen(be - 1); 
    	ans -= solve(l, 0, 1, 0); 
    	printf("%I64d\n", ans); 
    }
	return 0; 
} 
Scales

题意:有n个砝码,重量分别为 1 1 1, 2 2 2, …, 2 n − 1 2^{n-1} 2n1,还有一个重量为w的物品。从中选出一些砝码,求使天平平衡的方案个数。
n &lt; = 1 0 6 n &lt;= 10^6 n<=106
做法:一道相对独特的数位dp
首先,显然只放砝码无论如何不可能平衡
假设左物右码(中生物毒太深)
我们注意到,显然左边的砝码重量一旦确定,右边也随之确定。
于是我们从低往高枚举左边砝码重量每一位
d p [ i ] [ 0 ] dp[i][0] dp[i][0]表示第i位没有产生向前进位
d p [ i ] [ 1 ] dp[i][1] dp[i][1]表示第i位产生了向前进位
转移即可
复杂度 O ( n ) O(n) O(n)
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std; 

int n, l, d; 
char c[1000001]; 
int w[1000001], dp[1000001][2]; 
int main()
{
	int t; 
	scanf("%d", &t); 
	while(t--)
	{
		scanf("%d%d%d", &n, &l, &d); 
		scanf("%s", c + 1); 
		for(int i = 1; i <= l; i++)
			w[l - i + 1] = c[i] - '0'; 
		
		for(int i = l + 1; i <= n; i++)
			w[i] = 0; 
		memset(dp, 0, sizeof(dp)); 
		dp[0][0] = 1; 
		for(int i = 1; i <= n; i++)
		{
			if(w[i])
			{
				dp[i][0] = dp[i - 1][0]; 
				dp[i][1] = (dp[i - 1][1] + dp[i - 1][0]) % d; 
			}
			else
			{
				dp[i][0] = (dp[i - 1][0] + dp[i - 1][1]) % d; 
				dp[i][1] = dp[i - 1][1]; 
			}
		}
		printf("%d\n", dp[n][0]); 
	}
	return 0; 
} 
[ZJOI2010]数字计数

题意:求在[a,b]中的所有整数中,每个数码的出现次数
做法:裸数位dp
d p [ p o s ] [ t ] [ s m a l l ] [ z e r o ] dp[pos][t][small][zero] dp[pos][t][small][zero]表示当前位置为 p o s pos pos、所求数码为 t t t、是否已经比原数小、现在前面的位是否全是0
对每一个数码分开做即可
复杂度: O ( d i g i t ∗ 10 ∗ 4 ) O(digit * 10 * 4) O(digit104),实际上跑的飞快
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std; 

const int N = 15; 
long long f[N][N][2][2], cnt[N][2][2], p[N], pre[N]; 

long long dfs(int pos, int small, int zero, int t)
{
	if(!pos)
	{
		cnt[pos][small][zero] = 1; 
		return 0; 
	}
	if(~f[pos][t][small][zero])return f[pos][t][small][zero]; 
	
	int x = ((small) ? 9 : p[pos]); 
	f[pos][t][small][zero] = 0; 
	int pd = (~cnt[pos][small][zero]); 
	if(!pd)cnt[pos][small][zero] = 0; 
	for(int i = 0; i <= x; i++)
	{
		int ns = (small || i != p[pos]); 
		int nz = (zero || i); 
		f[pos][t][small][zero] += dfs(pos - 1, ns, nz, t); 
		if(!pd)cnt[pos][small][zero] += cnt[pos - 1][ns][nz]; 
		if(t == i && nz)f[pos][t][small][zero] += cnt[pos - 1][ns][nz]; 
	}
	return f[pos][t][small][zero]; 
}
long long dp(long long x, int y)
{
	if(x <= 0)return 0; 
	int s = 0; 
	while(x)
	{
		p[++s] = x % 10; 
		x /= 10; 
	}
	for(int i = 0; i <= s; i++)
		f[i][y][0][0] = f[i][y][1][0] = f[i][y][0][1] = f[i][y][1][1] = -1; 
	memset(cnt, -1, sizeof(cnt)); 
	return dfs(s, 0, 0, y); 
}
int main()
{
	long long a, b; 
	scanf("%lld%lld", &a, &b); 
	
	pre[0] = 1; 
	for(int i = 1; i <= N; i++)
		pre[i] = pre[i - 1] * 10; 
	for(int i = 0; i <= 9; i++)
		printf("%lld ", dp(b, i) - dp(a - 1, i)); 
	
	return 0; 
} 

好少啊qwq
先把锅留下
(待续)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值