一、數位DP概述
通常來說,數位 問題都是通過 解決的,因爲 的做法更容易理解,也能一定地簡化代碼。
對於 求解的數位 問題,其中設置的狀態爲 , 表示最後 位沒有填, 表示的是從 位之前繼承來的信息,然後 的數值表示僅考慮這 位所對答案產生的貢獻。
這 所表示的狀態通常需要根據題意來進行定義,比較個性化,也是數位 的核心難點。但數位 問題還是非常套路化的,你只需要根據題意想明白想要計算後 位的信息,到底需要從前幾位繼承哪些信息,想明白這個之後就可以直接套上 的模板進行求解了。
下面的習題給出的都是非套路化問題,具有一定的難度,初學者建議先寫一些模板題再來進行挑戰。
最後給出 問題的大致模板。(某一模板題的 代碼)
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
typedef long long ll;
using namespace std;
ll f[21][21][2010],a[30]; //左-右
ll dfs(int pos,int balan,int k,bool flag){
//位置 平衡點 左邊繼承來的數值 有無繼承
if(pos == 0){
if(k == 0) return 1;
else return 0;
}
if(!flag && f[pos][balan][k+1000] != -1) return f[pos][balan][k+1000];
ll ans = 0;
int end = flag?a[pos]:9;
rep(i,0,end){
ll tp = dfs(pos-1,balan,k+(pos-balan)*i,flag && i == end);
ans += tp;
}
if(!flag) f[pos][balan][k+1000] = ans;
return ans;
}
ll solve(ll n){
//求a數組
if(n == -1) return 0;
int pos = 0;
memset(a,0,sizeof a);
while(n){
a[++pos] = n%10;
n /= 10;
}
ll ans = 0;
rep(i,1,pos) ans += dfs(pos,i,0,1); //對每一個平衡點分開求
ans -= pos-1;
return ans;
}
int main()
{
//初始化
rep(i,0,20)
rep(j,0,20)
rep(k,0,2000) f[i][j][k] = -1;
//讀入
int _; scanf("%d",&_);
while(_--){
ll L,R; scanf("%lld%lld",&L,&R); L--;
printf("%lld\n",solve(R)-solve(L));
}
return 0;
}
二、數位DP系列習題
1. Daniel and Spring Cleaning
題意:
給出區間 ,查詢多少對 滿足如下條件。
思路:
的最後一題,比賽的時候有 來考慮這個問題。當時主要在思考這個題想要考察的是什麼內容,思考過數位 ,但是沒有做過 類型的數位 ,於是就沒有從這個角度繼續往下深入思考。因此剩下的大部分時間都在思考是不是一道結合某些數據結構的思維題,說實話,感覺數據結構開始限制我的思維了,什麼題都老從數據結構考慮,這樣非常容易被治,必須要改!
繼續回到該題,此題其實想要詢問的就是 的 對數。因此我們處理出一個 函數,表示 ,符合條件的 對數,所以最終答案就是 。
接下來就是如何計算 函數,其實維護 和 各自的數組,兩個 ,然後套上最基本的 板子,稍微改一下就可以過了。
總結:
此題其實應該是兩個數同時數位 的裸題,套上了一個最基本的容斥。而具體的函數實現過程還是比較套路的,並不難思考。
做不出來的原因也主要是沒有從數位 這個角度繼續往下深挖,是自己思考的片面性錯失了 。
代碼:
#include <bits/stdc++.h>
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define rep(i,a,b) for(int i = a; i <= b; i++)
typedef long long ll;
typedef double db;
const int N = 1e5+100;
const int M = 1e5+100;
const db EPS = 1e-9;
using namespace std;
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
ll f[40],a[40],b[40]; //左-右
ll dfs(int pos,bool flag1,bool flag2){
if(pos == 0) return 1;
if(!flag1 && !flag2 && f[pos] != -1) return f[pos];
ll ans = 0;
int end1 = flag1?a[pos]:1;
int end2 = flag2?b[pos]:1;
if(!flag1 && !flag2){
ans += 3ll*dfs(pos-1,0,0);
}
else if(!flag1){
rep(i,0,end2){
if(i == 0) ans += 2ll*dfs(pos-1,0,flag2 && i == end2);
else ans += dfs(pos-1,0,flag2 && i == end2);
}
}
else if(!flag2){
rep(i,0,end1){
if(i == 0) ans += 2ll*dfs(pos-1,flag1 && i == end1,0);
else ans += dfs(pos-1,flag1 && i == end1,0);
}
}
else{
rep(i,0,end1){
rep(j,0,end2){
if(i != 1 || j != 1) ans += dfs(pos-1,flag1 && i == end1,flag2 && j == end2);
}
}
}
if(!flag1 && !flag2) f[pos] = ans;
return ans;
}
ll solve(ll x,ll y){
if(x == -1 || y == -1) return 0;
int p1 = 0, p2 = 0;
memset(a,0,sizeof a);
memset(b,0,sizeof b);
while(x){
a[++p1] = x%2;
x /= 2;
}
while(y){
b[++p2] = y%2;
y /= 2;
}
return dfs(max(p1,p2),1,1);
}
int main()
{
int _; scanf("%d",&_);
memset(f,-1,sizeof f);
while(_--){
ll L,R; scanf("%lld%lld",&L,&R);
ll ans = solve(R,R)-solve(L-1,R)-solve(R,L-1)+solve(L-1,L-1);
printf("%lld\n",ans);
}
return 0;
}
2. Beautiful numbers
題意:
一個正整數被稱爲漂亮數,當且僅當其能夠被其所有非零數位整除,現需要求 中漂亮數的個數。
思路:
整除所有非零數位,即整除非零數位的 。因此我們需要在 的過程中,記錄出現數位的 ,以及記錄當前數字的大小。
這裏會出現一個問題,即當前數字非常大,直接記錄不可行,因此我們需要將當前數字對 取模,即 所有數的 。思考到這一步,剩下的就是一些代碼細節了,可以參考一下下述代碼。
代碼:
#include <bits/stdc++.h>
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define LOG1(x1,x2) cout << x1 << ": " << x2 << endl;
#define LOG2(x1,x2,y1,y2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << endl;
#define LOG3(x1,x2,y1,y2,z1,z2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << " , " << z1 << ": " << z2 << endl;
typedef long long ll;
typedef double db;
const int N = 1e5+100;
const int M = 1e5+100;
const db EPS = 1e-9;
using namespace std;
ll a[30],b[3000],tot,f[20][55][3000];
ll gcd(ll a,ll b){
return b == 0 ? a:gcd(b,a%b);
}
int find(ll x){
return lower_bound(b+1,b+1+tot,x)-b;
}
ll dfs(int pos,int lcm,ll m,bool flag){
if(pos == 0){
if(m%b[lcm] == 0) return 1;
else return 0;
}
if(!flag && f[pos][lcm][m] != -1) return f[pos][lcm][m];
int end = flag?a[pos]:9;
ll ans = 0;
rep(i,0,end){
if(i == 0) ans += dfs(pos-1,lcm,(m*10ll+i)%(2520ll),flag && i == end);
else{
ll tp = gcd(b[lcm],i);
ll hp = b[lcm]*i/tp;
int xp = find(hp);
ans += dfs(pos-1,xp,(m*10ll+i)%(2520ll),flag && i == end);
}
}
if(!flag) f[pos][lcm][m] = ans;
return ans;
}
ll solve(ll n){
if(n == 0) return 1;
int pos = 0;
memset(a,0,sizeof a);
while(n){
a[++pos] = n % 10;
n /= 10;
}
return dfs(pos,1,0,1);
}
int main()
{
rep(i,1,((1<<9)-1)){
ll ans = 1;
rep(j,1,9){
if(i&(1<<(j-1))){
ll tp = gcd(ans,j);
ans = ans*j/tp;
}
}
b[++tot] = ans;
}
sort(b+1,b+1+tot);
tot = unique(b+1,b+1+tot)-b-1;
rep(i,0,19)
rep(j,0,50)
rep(k,0,2900) f[i][j][k] = -1;
int _; scanf("%d",&_);
while(_--){
ll L,R; scanf("%lld%lld",&L,&R); L--;
printf("%lld\n",solve(R)-solve(L));
}
return 0;
}
3. 吉哥系列故事——恨7不成妻
題意:
尋找區間 中所有與 無關的數字的平方和。與 有關需要符合下述三個條件之一:
- 整數中某一位是
- 整數的每一位加起來的和是 的整數倍
- 這個整數是 的整數倍
思路:
如果這題只是單純地求個數,那就是一個普通數位 問題,但此題要求的是數字平方和,因此我們需要對每一個數位進行考慮。
當我們枚舉 位時,先遞歸到 位,返回一個結構體 ,表示若 位前全爲空時滿足題意的 、、,即個數、和、平方和。
設當前結構體爲 ,因此 ,,。
總結:
數位 的主要難點還是在於列出狀態,然後採用遞歸的思想來思考如何根據第 位的答案推出 位的答案。
代碼:
#include <bits/stdc++.h>
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define LOG1(x1,x2) cout << x1 << ": " << x2 << endl;
#define LOG2(x1,x2,y1,y2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << endl;
#define LOG3(x1,x2,y1,y2,z1,z2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << " , " << z1 << ": " << z2 << endl;
typedef long long ll;
typedef double db;
const int N = 1e5+100;
const int M = 1e5+100;
const ll mod = 1e9+7;
const db EPS = 1e-9;
using namespace std;
struct Node{
ll cnt,sum1,sum2; //個數、和、平方和
}dp[25][10][10];
ll a[30];
ll pow_mod(ll a, ll b, ll p){
ll base = a, ans = 1;
while(b){
if(b&1) ans = (ans*base)%p;
base = (base*base)%p;
b >>= 1;
}
return ans;
}
Node dfs(int pos, int pre1, int pre2, bool flag){ //pre1: 前面數位之和, pre2: 前面數位*貢獻之和
//flag = 1, 前面爲答案繼承而來
if(pos == 0){
if(pre1 == 0 || pre2 == 0) return {0,0,0};
else return {1,0,0};
}
if(!flag && dp[pos][pre1][pre2].cnt != -1) return dp[pos][pre1][pre2];
Node ans = {0,0,0}, tmp;
int end = flag?a[pos]:9;
rep(i,0,end){
if(i == 7) continue;
tmp = dfs(pos-1, (pre1+i)%7, (pre2*10ll+i)%7, flag && i == end);
ans.cnt = (ans.cnt+tmp.cnt)%mod;
ans.sum1 = (ans.sum1+tmp.sum1+(tmp.cnt*(ll)i)%mod*pow_mod(10,pos-1,mod)%mod)%mod;
ans.sum2 = (ans.sum2+tmp.sum2+(2ll*tmp.sum1*(ll)i)%mod*pow_mod(10,pos-1,mod)%mod)%mod;
ans.sum2 = (ans.sum2+(ll)i*(ll)i*pow_mod(10,pos-1,mod)%mod*pow_mod(10,pos-1,mod)%mod*tmp.cnt%mod)%mod;
}
if(!flag) dp[pos][pre1][pre2] = ans;
return ans;
}
ll solve(ll n){
int pos = 0;
memset(a,0,sizeof a);
while(n){
a[++pos] = n%(10ll);
n /= 10ll;
}
return dfs(pos,0,0,1).sum2;
}
int main()
{
rep(i,0,20)
rep(j,0,6)
rep(k,0,6) dp[i][j][k].cnt = -1;
int _; scanf("%d",&_);
while(_--){
ll L,R; scanf("%lld%lld",&L,&R); L--;
printf("%lld\n",(solve(R)-solve(L)+mod)%mod);
}
return 0;
}
4. Balanced Numbers
題意:
查找區間 中,符合如下條件的數字的個數:
- 所有出現過的偶數位,都出現了奇數次
- 所有出現過的奇數位,都出現了偶數次
思路:
這個問題主要的困難之處在於如何表示那些沒有出現過的數字,如果用二進制狀壓的方式顯然無法表示那些從未出現過的數字。
因此我們考慮使用三進制狀壓的方式, 表示沒有出現過, 表示出現了奇數次, 表示出現了偶數次。
因此我們設置狀態數組 ,表示還有 個位置沒有填數,之前填了的數字繼承下來的狀態爲 ,符合題幹條件的數的個數。
代碼:
#include <bits/stdc++.h>
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define LOG1(x1,x2) cout << x1 << ": " << x2 << endl;
#define LOG2(x1,x2,y1,y2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << endl;
#define LOG3(x1,x2,y1,y2,z1,z2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << " , " << z1 << ": " << z2 << endl;
typedef long long ll;
typedef double db;
const int N = 1e5+100;
const int M = 1e5+100;
const db EPS = 1e-9;
using namespace std;
ll f[21][60000],a[25],tp[25];
ll pow_mod(ll a,ll b){
ll base = a, ans = 1;
while(b){
if(b&1) ans *= base;
base *= base;
b >>= 1;
}
return ans;
}
ll dfs(int pos,ll S,bool flag){ //S-各數位出現次數的狀壓
//flag只是表示是否表示了該位的全部情況
if(pos == 0){
int jud = 1;
memset(tp,0,sizeof tp); int tot = -1;
while(S){
tp[++tot] = S % 3;
S /= 3;
}
rep(i,0,9){
if(tp[i] == 0) continue;
if(i%2){ //奇數
if(tp[i] != 2) {jud = 0; break;}
}
else{ //偶數
if(tp[i] != 1) {jud = 0; break;}
}
}
if(jud) return 1;
else return 0;
}
if(!flag && f[pos][S] != -1) return f[pos][S];
int end = flag?a[pos]:9;
ll ans = 0;
rep(i,0,end){
if(i == 0 && S == 0) ans += dfs(pos-1,S,flag && i == end); //不能把前面的前導0繼承過來
else{
int tot = -1, p = 0; ll hp = S;
while(hp){
++tot;
if(tot == i) {p = hp%3; break;}
hp /= 3ll;
}
hp = S;
if(p == 0 || p == 1) hp += pow_mod(3,i);
else hp -= pow_mod(3,i);
ans += dfs(pos-1,hp,flag && i == end);
}
}
// LOG3("pos",pos,"S",S,"ans",ans);
if(!flag) f[pos][S] = ans;
return ans;
}
ll solve(ll n){
if(n == 0) return 1;
int pos = 0;
memset(a,0,sizeof a);
while(n){
a[++pos] = n % 10;
n /= 10;
}
return dfs(pos,0,1);
}
int main()
{
ll base = pow_mod(3,10);
// LOG1("base",base);
rep(i,0,20)
rep(j,0,base)
f[i][j] = -1;
int _; scanf("%d",&_);
while(_--){
ll L,R; scanf("%lld%lld",&L,&R); L--;
printf("%lld\n",solve(R)-solve(L));
}
return 0;
}
/*
數位DP主要就是數位移動時,對答案貢獻的求取
在數位移動時,要仔細考慮各個細節點,包括前導0等信息
*/
5. Gift Pack
題意:
給出四個數,,表示 ,求 的最大值。
思路:
這個問題考察的是涉及三個數字的數位 ,首先我們來思考一下如何表示狀態。
此題是求取最大值,因此很明顯需要從數位的角度入手進行考慮,所以狀態的設置一定會包含各個數位的狀態,我們另 表示僅考慮最後 位,從前面轉移來的狀態爲 時答案的最大值。
- 爲 表示比 大
- 爲 表示比 小
- 爲 表示比 小
- 爲 表示比 小
然後在 的時候枚舉 位放的值,然後更新答案即可。代碼中將每個數拆成了二進制進行計算,因爲僅考慮 可以簡化問題。
代碼:
#include <bits/stdc++.h>
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define LOG1(x1,x2) cout << x1 << ": " << x2 << endl;
#define LOG2(x1,x2,y1,y2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << endl;
#define LOG3(x1,x2,y1,y2,z1,z2) cout << x1 << ": " << x2 << " , " << y1 << ": " << y2 << " , " << z1 << ": " << z2 << endl;
typedef long long ll;
typedef double db;
const int N = 1e5+100;
const int M = 1e5+100;
const db EPS = 1e-9;
using namespace std;
ll f[70][2][2][2][2],a1[70],a2[70],a3[70],a4[70];
ll pow_mod(ll a,ll b){
ll base = a, ans = 1;
while(b){
if(b&1) ans *= base;
base *= base;
b >>= 1;
}
return ans;
}
ll dfs(int pos,bool f1,bool f2,bool f3,bool f4){
//比L大, 比R小, 比A小, 比B小
if(pos == 0) return 0;
ll ans = f[pos][f1][f2][f3][f4];
if(ans != -1) return ans;
int b1 = 0, b2 = 1, d1 = 0, d2 = 1, e1 = 0, e2 = 1;
if(!f1 && a1[pos] != 0) b1 = 1;
if(!f2 && a2[pos] != 1) b2 = 0;
if(!f3 && a3[pos] != 1) d2 = 0;
if(!f4 && a4[pos] != 1) e2 = 0;
bool k1,k2,k3,k4;
rep(i,b1,b2)
rep(j,d1,d2)
rep(k,e1,e2){
k1 = f1, k2 = f2, k3 = f3, k4 = f4;
ll x = (i^j)+(j&k)+(k^i);
x *= pow_mod(2,pos-1);
// printf("********\n");
// LOG3("i",i,"j",j,"k",k);
// LOG2("pos",pos,"x",x);
if(i > a1[pos]) k1 = 1;
if(i < a2[pos]) k2 = 1;
if(j < a3[pos]) k3 = 1;
if(k < a4[pos]) k4 = 1;
ans = max(ans,x+dfs(pos-1,k1,k2,k3,k4));
}
return (f[pos][f1][f2][f3][f4] = ans);
}
ll solve(ll L,ll R,ll A,ll B){
memset(a1,0,sizeof a1);
memset(a2,0,sizeof a2);
memset(a3,0,sizeof a3);
memset(a4,0,sizeof a4);
int pos = 0;
while(L || R || A || B){
++pos;
a1[pos] = L & 1;
a2[pos] = R & 1;
a3[pos] = A & 1;
a4[pos] = B & 1;
L /= 2; R /= 2; A /= 2; B /= 2;
}
return dfs(pos,0,0,0,0);
}
int main()
{
int _; scanf("%d",&_);
while(_--){
rep(i,0,65)
rep(j,0,1)
rep(k,0,1)
rep(t1,0,1)
rep(t2,0,1)
f[i][j][k][t1][t2] = -1;
ll L,R,A,B; scanf("%lld%lld%lld%lld",&L,&R,&A,&B);
printf("%lld\n",solve(L,R,A,B));
}
return 0;
}
6. Gift Pack
題意:
給出區間 ,求所有數字中數位逆序對之和。
思路:
這是一個涉及到組合計數的數位 問題,初次看到肯定覺得很棘手,但是我們可以將這個問題不斷地進行分解。
我們先考慮 個位置,每個位置可以填 時,所有可能的數的數位逆序對之和。這個問題不難處理,直接從 中取兩個位置組成逆序隊,其它位置任意取,答案爲 。
然後再考慮如果 個位置,第一個位置不能爲 時的答案。我們先計算第一個位置和之後位置的貢獻,再計算後面 位置產生的貢獻。因此 。
處理完這兩個子問題之後,我們直接按照數位 的套路 求解即可。如果不是很清楚,可以查看代碼進行進一步瞭解。
思路:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
typedef long long ll;
using namespace std;
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
ll a[30],POS;
//快速冪
ll calc(ll a,ll b){
if(b < 0) return 0;
ll ans = 1, base = a;
while(b){
if(b&1) ans *= base;
base *= base;
b >>= 1;
}
return ans;
}
//數字長度爲pos且第一個數字不爲0
ll calc1(ll pos){
ll ans = 0;
ans += 36ll*(pos-1ll)*calc(10,pos-2); //第一個數不爲0,後續數字與其組成的貢獻
ans += 9ll*(pos-2ll)*(pos-1ll)*45ll*calc(10,pos-3)/2ll; //除第一個數字之外的貢獻
return ans;
}
//數字長度爲pos且第一個數字可以爲0
ll calc2(ll pos){
return pos*(pos-1ll)*45ll*calc(10,pos-2)/2ll;
}
ll dfs(int pos,int *base,bool flag,ll hp){
ll ans = 0;
if(pos == 0){
return hp;
}
if(!flag){
ans = calc2(pos);
rep(i,0,8) ans += (ll)base[i]*(ll)pos*(ll)(9-i)*calc(10,pos-1);
ans += hp*calc(10,pos);
return ans;
}
int end = a[pos];
ll tmp = 0;
rep(i,0,end){
if(POS == pos && i == 0) continue; //保證起始位不爲0
if(i > 0) tmp += (ll)base[i-1];
base[i]++;
ans += dfs(pos-1,base,flag && i == end,hp+tmp);
base[i]--;
}
return ans;
}
ll solve(ll n){
if(n <= 9) return 0;
POS = 0; memset(a,0,sizeof a);
while(n){
a[++POS] = n%10ll;
n /= 10ll;
}
int base[11];
rep(i,0,9) base[i] = 0;
ll ans = dfs(POS,base,1,0);
rep(i,2,POS-1) ans += calc1(i);
return ans;
}
int main(){
int _; scanf("%d",&_);
rep(Ca,1,_){
ll x,y; scanf("%lld%lld",&x,&y);
x--;
printf("Case %d: %lld\n",Ca,solve(y)-solve(x));
}
}