数位dp
Cat又在颓了,果然自己在家做题的感觉就是,读个题,啊我好想去翻题解啊……也不排除是因为遇到了数位dp这种鬼畜被吓着了……
“熟抄题解三百遍,不会做题也能A”
A. Windy 数 && B. 花神的数论题
一个dfs居然比暴力快那么多,可能是因为加了个记忆化吧。
套路:把原序列存到数组里,从高位到低位枚举的过程中判断是否恰好上限,不断把标记向后传递,能存入记忆化数组里的都是不含任何限制条件的dp值,因为限制条件的情况复杂过于细节,既不容易找到好的存储方式也一般不会被利用,就干脆不记录了。
注意:不要看到很多0之后加一个7的模数就是1e9+7吧,好好数数;如果一定要用快读读入长整形数,不要只改函数类型,还要把里面的变量类型也改改。WA 10 和 TLE 30还是很有区别的,暴力分也是分啊!
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 33; const int maxc = 1e5; int a[30], f[30][30];//f[i][j]中i表示数位,j表示当前位的上一位上的数字 inline int read() { int x = 0, f = 1; char ch = getchar(); while(ch > '9' || ch < '0') { if(ch == '-') { f = -1; } ch = getchar(); } while(ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch^48); ch = getchar(); } return x * f; } ll dfs(int pos, int lead, int flag, int num) { if(pos < 1) return 1; if(!flag && !lead && f[pos][num]!=-1) { return f[pos][num]; } int up = flag ? a[pos] : 9;//如果高位已经更小了,低位的数就不再具有限制意义 ll res = 0; for(int i=0; i<=up; i++)//枚举当前位填什么数 { if(!lead && abs(num-i)<2) continue; res += dfs(pos-1, lead&&i==0, flag&&i==a[pos], i); } if(!flag && !lead)//为什么flag成立不能转移? { f[pos][num] = res; } return res; } ll solve(ll x) { int k = 0; while(x) { a[++k] = x % 10;//设置上限 x /= 10; } return dfs(k, 1, 1, 0);//flag=1?因为两个最高位无论什么情况下都会有限制关系 } int main() { int a1 = read(), b = read(); memset(f, -1, sizeof(f)); printf("%lld\n", solve(b)-solve(a1-1)); return 0; }
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 33; const ll mod = 1e7 + 7;//默认取模1e9+7,Cat又大E了 ll n, f[55][55]; int a[55], len; inline ll read() { ll x = 0, f = 1;//改ll要改两处——暴力分都拿不全的Cat char ch = getchar(); while(ch > '9' || ch < '0') { if(ch == '-') { f = -1; } ch = getchar(); } while(ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch^48); ch = getchar(); } return x * f; } ll dfs(int p, int st, int limit)//st是指已经填了几个1? { if(p > len) return max(st, 1); if(f[p][st]!=-1 && !limit) return f[p][st]; ll ret = 1; int res; if(limit) res = a[len-p+1];//两个顺序是反着的,a里先存的是最低位 else res = 1; for(int i=0; i<=res; i++) { (ret*=dfs(p+1, i==1?st+1:st, limit&&(i==res)))%=mod; } if(!limit) f[p][st] = ret; return ret; } int main() { n = read(); while(n)//把n二进制拆分 { a[++len] = n&1; n >>= 1; } memset(f, -1, sizeof(f)); printf("%lld\n", dfs(1, 0, 1));//还是从高位到低位枚举 return 0; }
C. 手机号码
一下子这么多条件,忽然很蒙圈,我头一次见这么多维度的数组,这告诉我们要有足够的脑洞,我需要什么就开什么。处理三个连续的数的方法是记录上一个和上上个数是什么,一开始让我想的好复杂……还以为要像“启示录”一样记录连续两个和只有一个的情况,也有可能可以不过我没有尝试……果然我颓了。
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 33; const ll mod = 1e7 + 7; ll l, r, f[11][11][11][2][2][2][2]; int num[12], len; //p:当前位数,a:上一个数,b:上上个数,c:满足三个连续,d:满足比上限小 ll dfs(int p, int a, int b, bool c, bool d, bool _4, bool _8) { if(_4 && _8) return 0; if(p <= 0) return c; if(f[p][a][b][c][d][_4][_8] != -1) { return f[p][a][b][c][d][_4][_8]; } ll res = 0; int lim = !d ? num[p] : 9; for(int i=0; i<=lim; i++) { res += dfs(p-1, i, a, c||(i==b&&i==a), d||(i<lim), _4||(i==4), _8||(i==8)); } return f[p][a][b][c][d][_4][_8] = res;//这一可以直接取等的原因是限制条件都在数组里了?? } ll calc(ll x) { if(x < 1e10) return 0; memset(f, -1, sizeof(f)); len = 0;//记得清空啊 while(x) { num[++len] = x % 10; x /= 10; } /*for(len=0; x; x/=10) { num[++len] = x % 10; }*/ /*for(int i=1; i<=len; i++) { printf("%d ", num[i]); } printf("\n");*/ //=dfs(11, ...)考虑前导0的另一种方法,跳出循环;上一种方法是传个参数详见T1 ll res = 0; for(int i=1; i<=num[len]; i++) { res += dfs(10, i, 0, 0, i<num[len], i==4, i==8); } return res; } int main() { scanf("%lld%lld", &l, &r); printf("%lld\n", calc(r)-calc(l-1)); return 0; }
D. haha数
这题的难度在于一个数的合法性和整体有关,不能通过记录前某位是什么来直接判断,好像也不能传递,似乎就比上一个连续三位的限制条件更高级……提示说建议我们独立完成……求我心情的矛盾程度?它的位数很不确定好像把所有位都记录下来不太现实?如果改变套路从后往前填?那<=n的条件就不好保证……而且能由末位数字确定能不能整除的好像只有2和5,4要求两位,还有的好像只有除一下才知道……怎么办怎么办我想看题解但我才刚思考了没过15分钟……现在有15分钟了……25分钟了……
提示
数位 dp 入门题,大家最好独立完成。
算了,水平不够,我先跳了……
P3898 [湖南集训]大新闻
用到了数位dp中位数限制的思想,但并不是一个完全的数位dp的题。
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 5e4 + 2; const int mod = 1e9 + 7; const ll INF = 1e18; ll n, f; double p, s, p1, p2; inline ll read() { ll x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') { f = -1; } ch = getchar(); } while(ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch^48); ch = getchar(); } return x * f; } void print(double s) { while(s >= 10) { f++; s/=10; //printf("s = %.5lf\n", s); } printf("%.5lf %lld\n", s, f); } double solve1(ll n) { double ret = 0; ll Pow = 0, tmp = n-1; while(tmp != 0) { Pow++; tmp >>= 1; } for(int i=Pow; i>0; i--) { ll nw = (n>>i)*(1ll<<i-1)+min(n-((n>>i)<<i), 1ll<<i-1); double p = double(n-nw)/n; ret += (1.0-p)*(1ll<<(i-1))*p; } return ret * 2.0; } double solve2(ll n) { if(n == 1) return 0.0; double ret = 0.0; ll v = 1ll, delta, num, tmp = n-1; n--; while(v <= tmp) { v <<= 1; } //跳出循环后的v一定比tmp大了一位,-1回到本位 //它找的是异或结果的上限 delta = v - 1ll; v >>= 1;//<=tmp /* delta是能做的最大贡献,每一个x都对应了能让它生成最大贡献的y,不过这个y可能超过了 取值范围,n-1是一个数,它的最高位一定是1,就相当于讨论到“这一位上”,n-1是1的情况 只要x的最高位是0,后面的就可以随便选,y随着x被唯一确定也就等于x的情况数 x最高位是1一共有v种选法,选法总数是n+1,本来是n的,后来-1了就加回来 如果x的最高位是1,我还想让当前位做贡献,保险起见并且因为问题可以往后丢, 所以就算只有当前位贡献的,x=0并且y=1,前面的已经卡死,如果从这一位开始后面每个 y都和n-1对应相等,那就一定不会超范围,y只有一种情况,x有v种,贡献是v 所以这两个ret都是加的贡献值*方案数 */ ret += (double)delta*(n-v+1); ret += (double)v*v; num = v; delta >>= 1; /* 考虑完最高位,接着下面的,从一个问题中分出了子问题一定n-1的第i位为1, x的第i位为0导致有了后面的限制,否则所有的情况都已经算进来了,刚才需要拆分的问题只计算了 本位,不过y不需要和n-1的每一位都对应相等,只要高位有一位更小就能解锁 */ //我们考虑最高的i-1位和n-1的前i-1位相同时所有的x对答案的贡献 while(v != 1) { v >>= 1; delta >>= 1; if(n & v)//当前位上n-1=1 { ret += (double)num * v; //什么都没有固定,我只要单独的求可以使这一位有贡献的情况,x随便选 //任意的x一定对应一个合法的每一位与n-1对应相等的y使得当前位有贡献 ret += (double)(num>>1)*delta; //固定了当前位x只能选1,就少了一步有两种情况的分类讨论,/2 //由于上一个没有分类讨论,当前位有贡献的已经算过就不用再算了 //这大概是delta连续右移两次的原因 //为什么x=1和x=0的情况可以交叉合并?因为无论是哪一种,x对应的y都是唯一确定 //所以最终的情况数算的都是x可能的取值 num >>= 1; //问题规模减小 } else { //我还是想让当前位做贡献,后面y和n-1对应相等都不够, //必须固定之前的某一位x和y同时是0,因为x本来就是和y一一对应的 //所以只删掉x在那一位上是1的情况就好 ret += (double)(num>>1)*v; } } return ret / (double)(n+1); } int main() { n = read(); scanf("%lf", &p); p1 = solve1(n); p2 = solve2(n); s = (1.0-p)*p1+p*p2; printf("%.6lf\n", s); //print(s); return 0; }

浙公网安备 33010602011771号