动态规划·数位DP
数位DP
好东西(?),但我不会
直接上例题吧
呃呃呃:
- 后文中的 \(pos\) 默认是从低位往高位数的
- 为了在 \(n\) 的限制内,而后面方案的数都是随便选的,所以第 \(pos\) 位不会取到 \(n\) 的第 \(pos\) 位(保不齐就超了)
- 但这就会导致取不到 \(n\) ,所以要特判
【YbtOj】题解
A.B数计数
设状态 \(f_{pos,res,op}\) 表示从到第 \(pos\) 位、当前余数为 \(res\) 、“13”出现情况为 \(op\) 的B数个数:
- \(op=0\),表示没有“13”
- \(op=1\),表示当前填的数为“3”
- \(op=2\),表示已有“13”
其中 \(f_{0,0,0}=1\) ,状态从低位往高位转移即可。
在统计答案时,因为我们状态设的相当于是“$k $ 位数的B数个数”,为了给它有一个 \(n\) 的限制,我们要从高位往低位统计答案。
详:设 \(n\) 的第 \(pos\) 位是 \(s\) ,若状态第 \(pos\) 位取了 \(s\) ,那么第 \(pos+1\) 位也会有 $n $ 的限制;若第 \(pos\) 位取的是比 \(s\) 小的数,那么后面的数位从 \(0 \sim 9\) 就都可以取了。
这样统计是统计不到\(n\)的,所以最后要特判\(n\)
统计答案形象化(结合代码食用):
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=21;
int n;
int f[N][N][N];
int power[N];
void dp()
{
memset(f,0,sizeof f);
f[0][0][0]=1;
for (int pos=1;pos<=10;pos++)//在填的数位
for (int i=0;i<=9;i++)//试填pos位上的数
for (int res1=0;res1<13;res1++)//前pos位的余数
{
int res2=(res1-i*power[pos-1]%13+13)%13;//不包含第pos位的余数
if (i!=3) f[pos][res1][0]+=f[pos-1][res2][0];
if (i!=1&&i!=3) f[pos][res1][0]+=f[pos-1][res2][1];
if (i==3) f[pos][res1][1]+=f[pos-1][res2][0]+f[pos-1][res2][1];
if (i==1) f[pos][res1][2]+=f[pos-1][res2][1];
f[pos][res1][2]+=f[pos-1][res2][2];
}
}
int solve(int n)
{
int op1=0,res1=0,ans=0;//op1,res1针对n
for (int pos=10;pos>=1;pos--)
{
int s=n/power[pos-1];
for (int i=s-1;i>=0;i--)
{
int op2,res2=(res1+i*power[pos-1])%13;//res1:包括第pos+1位 res2:包括第pos位的余数 针对n
if (op1!=2)
{
if (i==3&&op1==1) op2=2;
else if (i==1) op2=1;
else op2=0;
}
else op2=2;
int res3=(13-res2)%13;//对后面几位的限制
if (op2==2) ans+=f[pos-1][res3][0]+f[pos-1][res3][1]+f[pos-1][res3][2];
else if (op2==1) ans+=f[pos-1][res3][2]+f[pos-1][res3][1];
else ans+=f[pos-1][res3][2];
}
if (op1!=2)
{
if (s==3&&op1==1) op1=2;
else if (s==1) op1=1;
else op1=0;
}
res1=(res1+s*power[pos-1])%13;
n%=power[pos-1];
}
if (op1==2&&res1==0) ans++;//特判n
return ans;
}
signed main()
{
power[0]=1;
for (int i=1;i<=9;i++) power[i]=power[i-1]*10;
while (scanf("%lld",&n)!=EOF)
{
dp();
printf("%lld\n",solve(n));
}
return 0;
}
B.区间圆数
因为“圆数”和二进制状态有关,所以设的是二进制下的状态、在二进制的基础上转移的。设状态 \(f_{pos,cnt}\) 表示填到第 \(pos\) 位,共出现 \(cnt\) 个 \(0\) 的个数。那么,状态转移显然为
在统计答案时,可以有一个类似“前缀和”的思想:计算 \([l,r]\) 内的圆数,就是 \([1,r]\) 内的圆数数量减去 \([1,l-1]\) 内的圆数数量,所以只需要解决 \([1,n]\) 内的圆数数量就可以求出答案了。
与上题类似,因为 \(n\) 的限制,统计时从高位往低位统计;因为无法统计到 \(n\) ,所以最后要特判 \(n\)。
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=35;
int l,r;
int f[N][N];//已填到第i位,共有j个0的数的个数
int s[N],cnt;
void dp()
{
f[0][0]=1;
for (int i=1;i<=32;i++)
for (int j=0;j<=i;j++)
{
if (!j) f[i][j]=f[i-1][j];
else f[i][j]=f[i-1][j]+f[i-1][j-1];
}
}
int solve(int n)
{
if (!n) return 0;
int cnt0=0,cnt1=0,ans=0,cnt=0;
while (n) s[++cnt]=n%2,n/=2;
for (int pos=cnt;pos>=1;pos--)
{
if (s[pos]&&pos!=cnt)//前面与n相同但这位为0的情况
{
cnt0++;
for (int i=0;i<=pos-1;i++)
{
if (i+cnt0>=cnt-cnt0-i) ans+=f[pos-1][i];
}
cnt0--;
}
if (pos!=cnt)//前面都是前导0的情况
{
for (int i=0;i<=pos-1;i++)
{
if (i>=pos-i) ans+=f[pos-1][i];
}
}
cnt0+=!s[pos];
cnt1+=s[pos];
}
if (cnt0>=cnt1) ans++;
return ans;
}
signed main()
{
dp();
scanf("%lld%lld",&l,&r);
int ans=solve(r)-solve(l-1);
printf("%lld",ans);
return 0;
}
C.数字计数
可以想到设状态 \(f_{pos,x}\) 表示填到第 \(pos\) 位,数码 \(x\) 共出现的次数。容易想到状态转移方程
然后就会发现 \(x\) 这一维没用。然后简化一下就是
直接转移就转移完了(感动哭
统计答案时,和上题同(“前缀和”思想)。然后统计就行
贴
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=16;
int l,r;
int power[N],f[N];//f[pos][j]:第pos位数码j出现的次数
int s[N],a[N],ans[N];
void dp()
{
power[0]=1;
for (int i=1;i<N;i++) power[i]=power[i-1]*10;
for (int pos=1;pos<N;pos++) f[pos]=f[pos-1]*10+power[pos-1];
}
void solve(int n)
{
memset(a,0,sizeof a);
int len=0,tmp=n;
while (tmp)//处理n的数位
{
s[++len]=tmp%10;
tmp/=10;
}
for (int pos=len;pos>=1;pos--)
{
for (int i=0;i<=9;i++)
{
a[i]+=f[pos-1]*s[pos];//pos位填0~s[pos]-1时pos位之后的贡献
if (i<=s[pos]-1) a[i]+=power[pos-1];//pos位填0_s[pos]-1时pos位产生的贡献
}
a[s[pos]]+=(n%power[pos-1]+1);//pos位填s[pos]时pos位产生的贡献 后面数因为有限制,所以不在此次计算贡献
a[0]-=power[pos-1];//减去这一位填0是前导0时多算的答案
}
}
signed main()
{
dp();
scanf("%lld%lld",&l,&r);
solve(r);
for (int i=0;i<=9;i++) ans[i]=a[i];
solve(l-1);
for (int i=0;i<=9;i++)
{
ans[i]-=a[i];//差分
printf("%lld ",ans[i]);
}
return 0;
}
D.数位翻转
据说很毒瘤,等我心情好了时候回来补
E.幸运666
要想办法转化成上面 \(n\) 的限制的数位dp,然后发现可以二分做!二分的 \(mid\) 就是数位限制。然后统计就没了
专吃太强了%%%
贴 记搜版
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=12;
const int M=1e12;
int T,n;
int len,s[N];
int f[N][5];//第二位状态表示6的个数
int dfs(int pos,int c,int lim)
{
if (!pos) return c==3;
if (!lim&&f[pos][c]!=-1) return f[pos][c];
int up=9,res=0;
if (lim) up=s[pos];
for (int i=up;i>=0;i--)
{
if (c==3) res+=dfs(pos-1,3,lim&&i==s[pos]);
else if (i==6) res+=dfs(pos-1,c+1,lim&&i==s[pos]);
else res+=dfs(pos-1,0,lim&&i==s[pos]);
}
if (!lim) f[pos][c]=res;
return res;
}
int check(int x)
{
len=0,memset(f,-1,sizeof f);
while (x)
{
s[++len]=x%10;
x/=10;
}
return dfs(len,0,1);
}
signed main()
{
scanf("%lld",&T);
while (T--)
{
scanf("%lld",&n);
int l=666,r=M;
while (l<r)
{
int mid=l+r>>1;
if (check(mid)>=n) r=mid;
else l=mid+1;
}
printf("%lld\n",l);
}
return 0;
}
F.乘积计算
直接做做完了(现在一看记搜确实更方便捏
贴 记搜版
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=50;
const int MOD=1e7+7;
int n;
int s[N],len;
int f[N][N];//f[pos][sum]:填到pos位共有sum个1的乘积
int dfs(int pos,int sum,int lim)//lim表示第pos位填的数是否有上限
{
if (pos==0) return max(1LL*1,sum);//以防全是0
if (!lim&&f[pos][sum]!=-1) return f[pos][sum];//已搜过
int up=1,ans=1;
if (lim) up=s[pos];//上限
for (int i=0;i<=up;i++) ans=(ans*dfs(pos-1,sum+(i==1),lim&&i==s[pos]))%MOD;
if (!lim) f[pos][sum]=ans;
return ans%MOD;
}
signed main()
{
scanf("%lld",&n);
while (n)//统计n二进制的数位
{
s[++len]=n%2;
n>>=1;
}
memset(f,-1,sizeof f);
printf("%lld",dfs(len,0,1));
return 0;
}
G.奶牛编号
“辛辛苦苦”敲了二分,但因为炸裂的输出 \(long\ long\) 也浅浅爆炸了。
所以这题要用组合数做。
考虑什么时候填1与0。在剩下 \(cnt1\) 位里填 \(cnt2\) 个"\(1\)"共有 \(C_{cnt1}^{cnt2}\) 种排列方案,若 \(C_{cnt1}^{cnt2}\) 比我们想要的排名要大,就说明 \(cnt1\) 的范围取大了,所以输出 \(0\) 继续往后看;反之,则填 \(1\) 并减去对应的排名。
然后就做完了
这和数位dp有个damn的关系啊
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e3+5;
int n,k,c[N][35];
void init()
{
c[0][0]=1;
for (int i=1;i<N;i++)
{
c[i][0]=1;
for (int j=1;j<=30;j++) c[i][j]=c[i-1][j]+c[i-1][j-1];
}
}
signed main()
{
init();
scanf("%lld%lld",&n,&k);
if (n==1)
{
for (int i=1;i<=k;i++) printf("1");
return 0;
}
if (k==1)
{
printf("1");
for (int i=2;i<=n;i++) printf("0");
return 0;
}
int cnt=k;
while (c[cnt][k]<=n) cnt++;
cnt--,k--;
printf("1");
for (int i=k;i<cnt;i++) n-=c[i][k];
for (int i=1;i<=cnt;i++)
{
if (k==0) printf("0");
else if (c[cnt-i][k]>=n) printf("0");
else
{
printf("1");
n-=c[cnt-i][k],k--;
}
}
return 0;
}
H.魔法数字
记录出现的一位数可以状压解决,然后就是余数问题了
有一个小结论(感性理解永远的神):$$p=lcm(p_1,p_2,…,p_n),(x\ mod\ p)\ mod\ p_i=x\ mod \ p_i$$
所以可以直接记录模 \(lcm(1,2,…,10)=2520\) 的余数,再最后判断就行。
于是乎可以设状态 \(f_{pos,opt,res}\) 表示填到了第 \(pos\) 位,数字出现情况为 \(opt\) ,当前 \(mod\ 2520\) 的余数为 \(res\) 的个数。
但是这样开的空间太大,时间复杂度会爆炸。怎么办捏
我们要想办法优化一维,发现第三维可以缩。显然,判断一个数是否是 \(5\) 的倍数只需要看它的末尾就行了,这样了话第三维开 \(504\) 就行了,转移的时候额外转一个上一个数 \(lst\) 判断就好了
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=20;
int k,l,r;
int s[N],len;
int f[N][(1<<9)+5][512];
inline int dfs(int pos,int opt,int ys,int lim,int lst)
{
if (!pos)
{
int cnt=0;
for (int i=1;i<=9;i++)
{
if (i!=5&&(opt&(1<<(i-1)))&&ys%i==0) cnt++;
}
if (opt&(1<<4)&&(lst==5||lst==0)) cnt++;
return cnt>=k;
}
if (!lim&&f[pos][opt][ys]!=-1) return f[pos][opt][ys];
int up=9,res=0;
if (lim) up=s[pos];
for (int i=0;i<=up;i++)
{
if (i) res+=dfs(pos-1,opt|(1<<(i-1)),(ys*10+i)%504,lim&&i==up,i);
else res+=dfs(pos-1,opt,(ys*10+i)%504,lim&&i==up,i);
}
if (!lim) f[pos][opt][ys]=res;
return res;
}
inline int solve(int x)
{
len=0;
while (x)
{
s[++len]=x%10;
x/=10;
}
return dfs(len,0,0,1,0);
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
memset(f,-1,sizeof f);
cin>>k>>l>>r;
cout<<solve(r)-solve(l-1);
return 0;
}
I.异或个数
据说是道史题,那我就不做了