19 年前聽 zlq 講課的時候學的東西,當時只會抄板子,如今來重學一波= =html
一個板子水一天題(不事node
給定參數 \(l,r\),求 \([l,r]\) 中不含前導零且相鄰兩個數字之差至少爲 \(2\) 的正整數的個數。
\(1\le l\le r\le 2\times 10^9\)。
1S,512MB。ubuntu
這是一個經典的數位 DP 的例子。其模型通常是給定一些對於數的限制條件,求在給定範圍內知足限制的數的貢獻。
經過數位 DP 通常能夠在 \(O(m\log_{10}{(n)})\) 的時間內解決此問題,其中 \(m\) 是數碼種類數,\(n\) 是取值的最大值。函數
首先將詢問 \([l,r]\) 內合法的數的個數拆成詢問 \([0\sim l-1]\) 和 \([0, r]\) 內合法的數的個數,以後考慮數位 DP。
數位 DP 有遞推 和 記憶化搜索兩種寫法,因爲記憶化搜索更容易理解與實現,咱們通常採用記憶化搜索解決此類問題。如下也僅介紹記憶化搜索的解法。優化
先考慮爆搜。考慮枚舉全部範圍內的數,搜索的同時檢查是否知足給定的限制條件。注意考慮前導零與是否達到枚舉的上界,其代碼以下所示:ui
int numlth, num[kN]; //儲存給定值的從高位到低位的十進制拆分。 //now_:當前填到第幾位; last_:now_ - 1 位填的數; //zero_:前 now_ - 1 位是否均爲 0; lim_:前 now_ - 1 位是否達到枚舉的上界(與 num 相同) int Dfs(int now_, int last_, bool zero_, bool lim_) { if (now_ > numlth) return 1; //當前枚舉的數合法 int ret = 0; //枚舉第 now_ 位填的數,up 爲該位填數的上界 for (int i = 0, up = lim_ ? num[now_] : 9; i <= up; ++ i) { if (abs(i - last_) < 2) continue ; if (zero_ && !i) ret += Dfs(now_ + 1, 11, true, lim_ && i == up); //前 now_ 位均爲 0 else ret += Dfs(now_ + 1, i, false, lim_ &&i == up); } return ret; } //ans[0, x] = Dfs(1, 11, true, true);
發現當枚舉的數前綴的性質相同,即 dfs 的四個參數相同時,dfs 的返回值相同。
好比當枚舉到 \(020\underline{?}??\) 和 \(010\underline{?}??\) 時,dfs 的參數均爲 (4, 0, false, false)
。表示它們前綴的性質相同,枚舉以後位數獲得的答案顯然也相同。
簡單記憶化便可避免重複枚舉過程。spa
//f[i][j][0/1][0/1] 表示 dfs(i, j, 0/1, 0/1) 的答案。 int numlth, num[kN], f[kN][kN][2][2]; int Dfs(int now_, int last_, bool zero_, bool lim_) { if (now_ > numlth) return 1; if (f[now_][last_][zero_][lim_] != -1) return f[now_][last_][zero_][lim_]; int ret = 0; for (int i = 0, up = lim_ ? num[now_] : 9; i <= up; ++ i) { if (abs(i - last_) < 2) continue ; if (zero_ && !i) ret += Dfs(now_ + 1, 11, true, lim_ && i == up); else ret += Dfs(now_ + 1, i, false, lim_ && i == up); } return f[now_][last_][zero_][lim_] = ret; } //ans[0, x] = Dfs(1, 11, true, true);
發現上述 dfs 的過程當中,\(\operatorname{lim} = 1\) 或 \(\operatorname{zero} = 1\) 的狀態只會被枚舉到 1 次,即只會重複調用 dfs(now_, last_, 0, 0)
。對這兩維的記憶化對減小枚舉次數是作負功的。
因而能夠經過特判去除這兩維,以下所示:code
//f[i][j] 表示 dfs(i, j, 0, 0) 的答案。 int Dfs(int now_, int last_, bool zero_, bool lim_) { if (now_ > numlth) return 1; if (!lim_ && f[now_][last_] != -1) return f[now_][last_]; int ret = 0; for (int i = 0, up = lim_ ? num[now_] : 9; i <= up; ++ i) { if (abs(i - last_) < 2) continue ; if (zero_ && !i) ret += Dfs(now_ + 1, 11, true, lim_ && i == up); else ret += Dfs(now_ + 1, i, false, lim_ && i == up); } if (!lim_) f[now_][last_] = ret; return ret; }
能夠感性理解特判的實際意義。若 dfs 的參數 \(\operatorname{lim} = 0\) 時,表示前綴比上界小,後面的位數能夠隨意填。所以前綴性質相同的全部子問題是徹底等價的,所以能夠記憶化。
\(\operatorname{zero} = 1\) 與 \(\operatorname{lim} = 0\) 必定是配套出現的,所以也能夠特判掉。htm
這樣時空複雜度均變爲了原來的 \(\frac{1}{4}\)。在其餘題目中也能夠套用此模板,將 0/1 維特判掉,減少時空複雜度。
可能有___出題人卡直接記憶化的寫法,好比這題:
引入問題的完整代碼。
//知識點:數位 DP /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #include <vector> #define LL long long const int kN = 15; //============================================================= int numlth, f[kN][kN]; std::vector <int> num; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Chkmax(int &fir, int sec) { if (sec > fir) fir = sec; } void Chkmin(int &fir, int sec) { if (sec < fir) fir = sec; } int Dfs(int now_, int last_, bool zero_, bool lim_) { if (now_ > numlth) return 1; if (!lim_ && f[now_][last_] != -1) return f[now_][last_]; int ret = 0; for (int i = 0, up = lim_ ? num[now_] : 9; i <= up; ++ i) { if (abs(i - last_) < 2) continue ; if (zero_ && !i) ret += Dfs(now_ + 1, 11, true, lim_ && i == up); else ret += Dfs(now_ + 1, i, false, lim_ && i == up); } if (!lim_) f[now_][last_] = ret; return ret; } int Calc(int val_) { num.clear(); num.push_back(0); for (int tmp = val_; tmp; tmp /= 10) num.push_back(tmp % 10); for (int i = 1, j = num.size() - 1; i < j; ++ i, -- j) { std::swap(num[i], num[j]); } numlth = num.size() - 1; memset(f, -1, sizeof (f)); return Dfs(1, 11, true, true); } //============================================================= int main() { int a = read(), b = read(); printf("%d\n", Calc(b) - Calc(a - 1)); return 0; }
給定兩個正整數 \(a\) 和 \(b\),求在 \([a,b]\) 中的全部整數中,每一個數碼各出現了多少次。
\(1\le a\le b\le 10^{12}\)。
1S,512MB。
與引入問題不一樣的是,這題要求的是數碼的數量,限制了每一個數的貢獻,求貢獻和。
套路相似,考慮對每一個數碼分開求解,dfs 時記錄已枚舉前綴的貢獻量。
設 Dfs(int now_, LL sum_, bool zero_, bool lim_, int digit_)
表示前 \(\operatorname{now} - 1\) 位含有數碼 \(\operatorname{digit}\) 的數量爲 \(\operatorname{sum}\)、前綴是否全爲前導零、前綴是否達到上界,知足上述條件的全部數中數碼 \(\operatorname{digit}\) 的數量。
邊界是搜索到第 \(\operatorname{length}+1\) 位,此時返回 \(\operatorname{sum}\) 的值。
與套路相似地,發現一些 \(\operatorname{now}\) 和 \(\operatorname{sum}\) 相等的搜索狀態會被重複訪問,簡單記憶化便可。
總複雜度 \(O(10^2\log_{10}(n))\) 級別。
//知識點:數位 DP /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #include <vector> #define LL long long const int kN = 20; //============================================================= LL numlth, f[kN][kN]; std::vector <int> num; //============================================================= inline LL read() { LL f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Chkmax(int &fir, int sec) { if (sec > fir) fir = sec; } void Chkmin(int &fir, int sec) { if (sec < fir) fir = sec; } LL Dfs(int now_, LL sum_, bool zero_, bool lim_, int digit_) { if (now_ > numlth) return sum_; if (!lim_ && f[now_][sum_] != -1) return f[now_][sum_]; LL ret = 0; for (int i = 0, up = lim_ ? num[now_] : 9; i <= up; ++ i) { if (zero_ && !i) ret += Dfs(now_ + 1, sum_, true, lim_ && i == up, digit_); else ret += Dfs(now_ + 1, sum_ + (i == digit_), false, lim_ && i == up, digit_); } if (!lim_) f[now_][sum_] = ret; return ret; } LL Calc(LL val_, int digit_) { num.clear(); num.push_back(0); for (LL tmp = val_; tmp; tmp /= 10) num.push_back(tmp % 10); for (int i = 1, j = num.size() - 1; i < j; ++ i, -- j) std::swap(num[i], num[j]); numlth = num.size() - 1; memset(f, -1, sizeof (f)); return Dfs(1, 0, true, true, digit_); } //============================================================= int main() { LL a = read(), b = read(); for (int i = 0; i <= 9; ++ i) printf("%lld ", Calc(b, i) - Calc(a - 1, i)); return 0; }
還有一種考慮每一個位置填入指定數碼後對應的數的個數的無腦寫法,看代碼就能看懂。
//知識點:暴力 /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #define LL long long const int kN = 13; //============================================================= LL f[kN]; //============================================================= inline LL read() { LL f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Chkmax(int &fir, int sec) { if (sec > fir) fir = sec; } void Chkmin(int &fir, int sec) { if (sec < fir) fir = sec; } LL Calc(LL val_, LL digit_) { LL ret = (!digit_); for (LL tmp = val_, pow10 = 1; tmp; tmp /= 10ll, pow10 *= 10ll) { LL pre = tmp / 10ll + 1; if (! digit_) { if (pre == 1) continue; if (0 < tmp % 10) ret += (pre - 1ll) * pow10; if (0 == tmp % 10) ret += (pre - 2ll) * pow10 + val_ % pow10 + 1; continue; } if (digit_ > tmp % 10) ret += (pre - 1ll) * pow10; if (digit_ == tmp % 10) ret += (pre - 1ll) * pow10 + val_ % pow10 + 1; if (digit_ < tmp % 10) ret += pre * pow10; } return ret; } //============================================================= int main() { LL a = read(), b = read(); for (int i = 0; i <= 9; ++ i) printf("%lld ", Calc(b, i) - Calc(a - 1, i)); return 0; }
給定兩個正整數 \(a\) 和 \(b\),求在 \([a,b]\) 中的全部整數中,各位數之和能整除原數的數的個數。
\(1\le a\le b\le 10^{18}\)。
3S,512MB。
考慮到各位數之和與原數在 dfs 中都是變量,不易檢驗合法性。但發現各位數之和不大於 \(9\times 12\),考慮先枚舉各位數之和,再在 dfs 時維護前綴的餘數,以檢查是否合法。
一樣設 Dfs(int now_, int sum_, int p_, bool zero_, bool lim_, int val_)
,其中 \(\operatorname{sum}\) 爲前綴的各數位之和,\(p\) 爲原數模 \(\operatorname{val}\) 的餘數。
邊界是搜索到第 \(\operatorname{length}+1\) 位,此時返回 \([\operatorname{sum}=\operatorname{val} \land \, p = 0]\)。
對數位和和餘數簡單記憶化便可,總複雜度 \(O(2\cdot10^2\log_{10}^3(n))\) 級別。
//知識點:數位 DP /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #include <vector> #define LL long long const int kN = 20; //============================================================= int numlth; LL f[kN][9 * kN][9 * kN]; std::vector <int> num; //============================================================= inline LL read() { LL f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Chkmax(int &fir, int sec) { if (sec > fir) fir = sec; } void Chkmin(int &fir, int sec) { if (sec < fir) fir = sec; } LL Dfs(int now_, int sum_, int p_, bool zero_, bool lim_, int val_) { if (now_ > numlth) return (sum_ == val_ && !p_); if (!lim_ && f[now_][sum_][p_] != -1) return f[now_][sum_][p_]; LL ret = 0; for (int i = 0, up = lim_ ? num[now_] : 9; i <= up; ++ i) { if (zero_ && !i) ret += Dfs(now_ + 1, sum_, 10 * p_ % val_, true, lim_ && i == up, val_); else ret += Dfs(now_ + 1, sum_ + i, (10 * p_ + i) % val_, false, lim_ && i == up, val_); } if (!zero_ && !lim_) f[now_][sum_][p_] = ret; return ret; } LL Calc(LL val_) { num.clear(); num.push_back(0); for (LL tmp = val_; tmp; tmp /= 10) num.push_back(tmp % 10); for (int i = 1, j = numlth = num.size() - 1; i < j; ++ i, -- j) { std::swap(num[i], num[j]); } LL ret = 0; for (int i = 1; i <= 9 * numlth; ++ i) { memset(f, -1, sizeof (f)); ret += Dfs(1, 0, 0, true, true, i); } // printf("%lld %lld\n", val_, ret); return ret; } //============================================================= int main() { LL a = read(), b = read(); printf("%lld\n", Calc(b) - Calc(a - 1)); return 0; }
給定兩個正整數 \(a\) 和 \(b\),求在 \([a,b]\) 中的全部整數中,存在長度至少爲2的迴文子串的數的個數。
\(1\le a< b\le 10^{1000}\)。
1S,128MB。
存在長度至少爲2的迴文子串等價於沒有連續相等的三位,dfs 時記錄前兩位便可。代碼 Link。
給定兩個正整數 \(a\) 和 \(b\),求在 \([a,b]\) 中的全部整數中,至少有三個相鄰的相同數字,且 8 和 4 不一樣時存在的數的個數。
\(10^{10}\le a\le b\le 10^{11}\)。
1S,256MB。
狀態多設幾維便可,記錄前兩位,前綴中是否有有三個相鄰的相同數字,前綴中是否有 8,前綴中是否有 4。代碼 Link。
給定一正整數 \(a\),求在 \([1,a]\) 中的全部整數的二進制拆分中 1 的個數的乘積。
\(1\le a \le 10^{15}\)。
1S,128MB。
二進制拆分 \(a\),同「AHOI2009」同類分佈,枚舉二進制中 1 的個數 dfs 便可。
注意不要亂取模。代碼 Link。
給定一個整數 \(n\),一大小爲 \(m\) 的數字串集合 \(s\)。
求不以 \(s\) 中任意一個數字串做爲子串的,不大於 \(n\) 的數字的個數。
\(1\le n\le 10^{1201}\),\(1\le m\le 100\),\(1\le \sum |s_i|\le 1500\)。\(n\) 沒有前導零,\(s_i\) 可能存在前導零。
1S,128MB。
題目要求不以 \(s\) 中任意一個數字串做爲子串,想到這題:「JSOI2007」文本生成器。首先套路地對給定集合的串構建 ACAM,並在 ACAM 上標記全部包含集合內的子串的狀態。
以後考慮在 ACAM 上模擬串匹配的過程作數位 DP。發現前綴所在狀態儲存了前綴的全部信息,能夠將其做爲 dfs 的參數。
設 Dfs(int now_, int pos_, bool zero_, bool lim_) {
表示前綴匹配到的 ACAM 的狀態爲 \(\operatorname{pos}\) 時,合法的數字的數量。轉移時沿 ACAM 上的轉移函數轉移,避免轉移到被標記的狀態。
存在 \(\operatorname{trans}(0, 0) = 0\),這樣直接 dfs 也能順便處理不一樣長度的數字串。
總複雜度 \(O(\log_{10}(n)\sum |s_i|)\) 級別。
//知識點:ACAM,數位 DP /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #include <queue> #define LL long long const int kN = 1500 + 10; const int mod = 1e9 + 7; //============================================================= int n, m, ans; char num[kN], s[kN]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Chkmax(int &fir, int sec) { if (sec > fir) fir = sec; } void Chkmin(int &fir, int sec) { if (sec < fir) fir = sec; } namespace ACAM { const int kSigma = 10; int node_num, tr[kN][kSigma], last[kN], fail[kN]; int f[kN][kN]; bool tag[kN]; void Insert(char *s_) { int u_ = 0, lth = strlen(s_ + 1); for (int i = 1; i <= lth; ++ i) { if (! tr[u_][s_[i] - '0']) tr[u_][s_[i] - '0'] = ++ node_num; u_ = tr[u_][s_[i] - '0']; last[u_] = s_[i] - '0'; } tag[u_] = true; } void Build() { std:: queue <int> q; for (int i = 0; i < kSigma; ++ i) { if (tr[0][i]) q.push(tr[0][i]); } while (!q.empty()) { int u_ = q.front(); q.pop(); tag[u_] |= tag[fail[u_]]; for (int i = 0; i < kSigma; ++ i) { int v_ = tr[u_][i]; if (v_) { fail[v_] = tr[fail[u_]][i]; q.push(v_); } else { tr[u_][i] = tr[fail[u_]][i]; } } } } int Dfs(int now_, int pos_, bool zero_, bool lim_) { if (now_ > n) return 1; if (!zero_ && !lim_ && f[now_][pos_] != -1) return f[now_][pos_]; int ret = 0; for (int i = 0, up = lim_ ? num[now_] - '0': 9; i <= up; ++ i) { int v_ = tr[pos_][i]; if (tag[v_]) continue; if (zero_ && !i) ret += Dfs(now_ + 1, 0, true, lim_ && i == num[now_] - '0'); else ret += Dfs(now_ + 1, v_, false, lim_ && i == num[now_] - '0'); ret %= mod; } if (!zero_ && !lim_) f[now_][pos_] = ret; return ret; } int DP() { memset(f, -1, sizeof (f)); return Dfs(1, 0, true, true); } } //============================================================= int main() { scanf("%s", num + 1); n = strlen(num + 1); m = read(); for (int i = 1; i <= m; ++ i) { scanf("%s", s + 1); ACAM::Insert(s); } ACAM::Build(); printf("%d\n", ACAM::DP()); return 0; }
鳴謝: