2020-08-18 集訓題目題解
講數位dp,然後發現自己學暴了,這裡挑幾道有意思的題目記錄一下,以免將來死得太慘。
windy數
題目大意
定義 windy 數為滿足相鄰兩位差值 \(\le 2\) 的數,給出 \(l,r\) ,求出 \([l,r]\) 內有多少 windy 數。
\(l,r\le 2\times 10^9\)
思路
我™ 這樣一個板子題調了一個小時,果然是我自己菜爆了。。。
我們可以設 \(dp[i][x]\) 表示確定了前 \(i\) 位並且第 \(i\) 位為 \(x\) 時的合法方案數,然後直接記憶化搜尋就好了。下面是一些數位 dp 的細節
-
注意前面幾位跟極限相同的情況需要特殊判斷
-
還有前導零的情況需要特殊判斷
原因就是這兩種情況會限制後面幾位的選擇。
\(\texttt{Code}\)
#include <bits/stdc++.h> using namespace std; #define Int register int #define MAXN template <typename T> inline void read (T &t){t = 0;char c = getchar();int f = 1;while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}while (c >= '0' && c <= '9'){t = (t << 3) + (t << 1) + c - '0';c = getchar();} t *= f;} template <typename T,typename ... Args> inline void read (T &t,Args&... args){read (t);read (args...);} template <typename T> inline void write (T x){if (x < 0){x = -x;putchar ('-');}if (x > 9) write (x / 10);putchar (x % 10 + '0');} int len,nnum[11],dp[11][10]; int Abs (int x){return x < 0 ? -x : x;} int dfs (bool up,bool zero,int now,int x){ if (!now) return 1; if (!up && zero && ~dp[now][x]) return dp[now][x]; int res = 0,upup = up ? nnum[now] : 9; for (Int i = 0;i <= upup;++ i)if (Abs (x - i) >= 2 || !zero) res += dfs (up & (i == upup),zero || i,now - 1,i); if (!up && zero) dp[now][x] = res; return res; } int calc (int n){ int tmp = n,res = 0;len = 0; while (tmp) nnum[++ len] = tmp % 10,tmp /= 10; memset (dp,-1,sizeof (dp)); return dfs (1,0,len,-2); } signed main(){ int a,b;read (a,b); write (calc (b) - calc (a - 1)),putchar ('\n'); return 0; }
恨 7 不成妻
題目大意
定義一個數字與 \(7\) 有關當且僅當一下幾種情況:
-
數字中含有 \(7\)
-
各位數字之和為 \(7\) 的倍數
-
為 \(7\) 的倍數
給出 \([l,r]\) ,求出該區間內與 \(7\) 無關的數字的平方和。
\(l\le r\le 10^{18}\),答案對 \(10^9+7\) 取模。
思路
顯然我們沒有辦法直接搞了。但是我們發現假設當前位為 \(x\) ,後面為 \(y\) ,那麼答案就是 \((x+y)^2=x^2+2xy+y^2\) ,然後我們發現 \(\text{answer} =x^2\times \sum+ 2x\times y+\sum y^2\)
\(\texttt{Code}\)
#include <bits/stdc++.h>
using namespace std;
#define Int register int
#define mod 1000000007
#define int long long
template <typename T> inline void read (T &t){t = 0;char c = getchar();int f = 1;while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}while (c >= '0' && c <= '9'){t = (t << 3) + (t << 1) + c - '0';c = getchar();} t *= f;}
template <typename T,typename ... Args> inline void read (T &t,Args&... args){read (t);read (args...);}
template <typename T> inline void write (T x){if (x < 0){x = -x;putchar ('-');}if (x > 9) write (x / 10);putchar (x % 10 + '0');}
int len,pw[19],nnum[19];
int mul (int a,int b){return 1ll * a * b % mod;}void Mul (int &a,int b){a = mul (a,b);}
int dec (int a,int b){return a >= b ? a - b : a + mod - b;}void Dec (int &a,int b){a = dec (a,b);}
int add (int a,int b){return a + b >= mod ? a + b - mod : a + b;}void Add (int &a,int b){a = add (a,b);}
struct node{
int cnt,sum,sqr;bool exi;//分別維護0次方、1次方、2次方,是否訪問過
void clear (){cnt = sum = sqr = exi = 0;}
}dp[19][7][7];
node dfs (bool lim,int now,int sum1,int sum2){
if (!now){
node tmp;tmp.clear();
if (sum1 && sum2) tmp.cnt = 1;
return tmp;
}
if (!lim && dp[now][sum1][sum2].exi) return dp[now][sum1][sum2];
int up = lim ? nnum[now] : 9;node res;res.clear();
for (Int i = 0;i <= up;++ i){
if (i == 7) continue;
int tmp = mul (pw[now - 1],i);
node nxt = dfs (lim & (i == up),now - 1,(sum1 + i) % 7,(sum2 * 10 + i) % 7);
Add (res.cnt,nxt.cnt),Add (res.sum,add (nxt.sum,mul (tmp,nxt.cnt))),Add (res.sqr,add (mul (nxt.cnt,mul (tmp,tmp)),add (mul (2 * tmp,nxt.sum),nxt.sqr)));
}
if (!lim) dp[now][sum1][sum2] = res,dp[now][sum1][sum2].exi = 1;
return res;
}
int calc (int n){
memset (dp,0,sizeof (dp));
int tmp = n;len = 0;
while (tmp) nnum[++ len] = tmp % 10,tmp /= 10;
return dfs (1,len,0,0).sqr;
}
signed main(){
int T;read (T);
pw[0] = 1;for (Int i = 1;i <= 18;++ i) pw[i] = mul (pw[i - 1],10);
while (T --> 0){
int l,r;read (l,r);
write (dec (calc (r),calc (l - 1))),putchar ('\n');
}
return 0;
}
tickets
題目大意
給出 \(l,r,k\) ,將 \([l,r]\) 劃分成某些段,使得每一段上面的編號的每位數字之和不小於 \(k\) 並且儘可能小。求出分成的段數。
\(l\le r\le 10^{18},k\le 1000\)
思路
為了更好幫助理解題意,比如 \(l=40,k=11\) ,那麼 \(40,41,42\) 就會被劃分成一段,因為 \(4+0+4+1+4+2\ge 11\) 而且 \(4+0+4+1<11\)。
私認為是很巧妙的一道題。我們可以考慮設 \(dp[i][s1][s2]\) 表示考慮第 \(i\) 位當前編號,\(s1\) 為當前編號產生的每位數字之和,\(s2\) 表示已經劃分出來的貢獻。然後我們就可以進行合併,具體見程式碼。
\(\texttt{Code}\)
#include <bits/stdc++.h>
using namespace std;
#define Int register int
#define int long long
template <typename T> inline void read (T &t){t = 0;char c = getchar();int f = 1;while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}while (c >= '0' && c <= '9'){t = (t << 3) + (t << 1) + c - '0';c = getchar();} t *= f;}
template <typename T,typename ... Args> inline void read (T &t,Args&... args){read (t);read (args...);}
template <typename T> inline void write (T x){if (x < 0){x = -x;putchar ('-');}if (x > 9) write (x / 10);putchar (x % 10 + '0');}
int l,r,K,tmp,lnum[19],rnum[19];
struct node{
int a,b;bool exi;
node (){a = b = exi = 0;}
node (int _a,int _b,bool _exi){a = _a,b = _b,exi = _exi;}
}dp[20][185][1010];
void Merge (node &x,node y){
x.a += y.a,x.b = y.b;
}
node dfs (bool lim1,bool lim2,int now,int sum,int rem){
node ans = node (0,rem,0);
if (dp[now][sum][rem].exi && !lim1 && !lim2) return dp[now][sum][rem];
if (!now){
if (sum + rem >= K) ans = node (1,0,0);
else ans = node (0,sum + rem,0);
}
else{
int down = lim1 ? lnum[now] : 0,up = lim2 ? rnum[now] : 9;
for (Int i = down;i <= up;++ i) Merge (ans,dfs (lim1 & (i == down),lim2 & (i == up),now - 1,sum + i,ans.b));
}
if (!lim1 && !lim2) dp[now][sum][rem] = ans,dp[now][sum][rem].exi = 1;
return ans;
}
signed main(){
read (l,r,K);
tmp = l;int len1 = 0;while (tmp) lnum[++ len1] = tmp % 10,tmp /= 10;
tmp = r;int len2 = 0;while (tmp) rnum[++ len2] = tmp % 10,tmp /= 10;
write (dfs (1,1,18,0,0).a),putchar ('\n');
return 0;
}