最近囫囵吞枣,吃了几个dp类型的知识点。就算是临阵磨枪吧,毕竟神仙打架的杭州快开始了。 打算收官完dp,就去回头巩固那些思维上的东西了。 到时候算法题没做出来,我也不怪自己,毕竟时间少,积累少。 要是思维没想到,感觉接受不了。
.
- 数位dp
:这类题 一般都是要求 l-r中 满足某一个条件的数有多少,一般这个条件都可以拆为: 以j为开头的i位的数 ,满足条件的有多少:dp[i][j][k]。
第三位可以用来做别的事情。辅助去算那个满足的条件。
T1.hdu2089
题意:条件即为: 数字中不要出现 “62” 以及“4”。
Input
输入的都是整数对n、m(0< n≤m<1000000),如果遇到都是0的整数对,则输入结束。
Output
对于每个整数对,输出一个不含有不吉利数字的统计个数,该数值占一行位置。
这是一道很简单的入门题:
我们甚至只要二维就可以解决:
设置dp[i][j] 表示以j开头i位的满足条件的数字有多少?
在遍历过程中 ,符合条件的去做就可以了:
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
const double PI = acos(-1.0);
const double e = exp(1.0);
#define ll __int64
template<class T> T gcd(T a, T b) { return b ? gcd(b, a % b) : a; }
template<class T> T lcm(T a, T b) { return a / gcd(a, b) * b; }
using namespace std;
/*
一个一个去理解
比如统计15
第一位 dp[2][0]=9 即 00 01 02 03 05 06 07 08 09
第二位 dp[1][0~4] 有 0 1 2 3 有四个 ,而这四个 其实是: 10 11 12 13,是建立在前一个1上的
所以一共有13个
但是并没有去统计 15 这个本身的数
*/
int dp[10][10]; //dp[i][j] 以j开头 并且长度为i的 个数
void init(){
memset(dp,0,sizeof(dp));
for(int i=0;i<=9;i++)
dp[1][i]=1;
for(int i=1;i<=7;i++){ // 数字长度最多为7
for(int j=0;j<=9;j++){ //第i位 j
for(int k=0;k<=9;k++){ //第i-1 位 k
if(j==4 || k==4) continue;
if(j==6 && k==2) continue;
dp[i][j]+=dp[i-1][k]; //如此一来 j......(i位)= (j) 0-9 .... (i-1位) 的和
}
}
}
}
char a[16],b[15];
int count(char *str,int v){
int len=strlen(str);
int ans=0;
for(int i=0;i<len;i++){
int id=str[i]-'0'; // 第len-i 位 id
//这一步 i没有建立 和i-1位的联系, 只是去计算开头为0-i-1 的多少位的合法数
// 可以说这一步是建立在 a[i-1] 就是a[i-1]的情况下的
for(int j=0;j<id;j++){ //统计所有这一位比 id 小的合法数字
if(j!=4 && !(str[i-1]=='6' && j==2)){
ans+=dp[len-i][j];
}
}
if(str[i]=='4' || (str[i-1]=='6' && str[i]=='2')){ //后面都不用再统计了
break;
}
// printf("v=%d i=%d %d\n",v,i,ans);
}
int flag=1;
for(int i=0;i<len;i++){
if(str[i]=='4')
flag=0;
if(str[i]=='6' && str[i+1]=='2')
flag=0;
}
if(v==1)
flag=0;
ans+=flag;
return ans;
}
int main(){
//freopen("1.txt","r",stdin);
while(~scanf("%s %s",a,b)){
init();
int lena=strlen(a);
int lenb=strlen(b);
if(lena==lenb && lena==1){
if(a[0]=='0' && b[0]==a[0]){
return 0;
}
}
int l=0,r=0;
l=count(a,1);
r=count(b,2);
// printf("%d %d\n",l,r);
//判断 r 是否可行,需要这一步了,
printf("%d\n",r-l);
}
return 0;
}
这是最普通,最简单,往往也是最直接有效的解题手段,根据自己的思维直接去解题即可。
hdu 3652
题意依然很简单: l,r中满足条件的数。
r< 1e9
条件:该数既要能被13整除,也要含有“13”;
这题比刚刚那题多了一个条件,除了一定包含XX,还需要能被某个数整除。
这里我们自然而然的多开一维,甚至多开两维,不影响大局,却还能够方便我们的操作,以及拓宽我们的思路。
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
const double PI = acos(-1.0);
const double e = exp(1.0);
#define ll __int64
template<class T> T gcd(T a, T b) { return b ? gcd(b, a % b) : a; }
template<class T> T lcm(T a, T b) { return a / gcd(a, b) * b; }
using namespace std;
ll dp[12][12][13][2];
//dp 以j开头的i位数 余数为k 前面没有出现13 为0 ...前面已经出现过13 为1
/*
可以把这个dp 当做两个dp来看
*
*/
int digit[12] , cnt ;
ll f(int n,int j) //j *1e(n)
{
ll i , k = j ;
for(i = 0 ; i < n ; i++)
k *= 10 ;
return k ;
}
void init()
{
int i , j , k , l , temp ;
memset(dp,0,sizeof(dp)) ;
for(j = 0 ; j < 10 ; j++)
dp[1][j][ j%13 ][0] = 1 ; //初始化
for(i = 2 ; i <= 10 ; i++) //一共i位
{
for(j = 0 ; j < 10 ; j++) //这一位为 j
{
temp = f(i-1,j) ; //j*1e(i-1) 这一位贡献j00...0
for(k = 0 ; k < 10 ; k++) //k 代表后一位的数字是多少 即: jk......
{
for(l = 0 ; l < 13 ; l++) //遍历 后一位所有的余数,得到这一位的余数
{
dp[i][j][ (temp+l)%13 ][1] += dp[i-1][k][l][1] ; //前面出现过,这里也出现
if( j == 1 && k == 3 ) // 这一次出现
dp[i][j][ (temp+l)%13 ][1] += dp[i-1][k][l][0] ; //前面没出现过,这里出现了
else
dp[i][j][ (temp+l)%13 ][0] += dp[i-1][k][l][0] ; //+前面也没有出现过的
}
}
}
}
return ;
}
ll solve(ll temp)
{
memset(digit,0,sizeof(digit)) ;
cnt = 0 ;
while( temp )
{
digit[++cnt] = temp%10 ;
temp /= 10 ;
}
ll ans = 0 , i , j, k ;
for(j = 0 ; j < digit[cnt] ; j++) //这个不能忘掉
ans += dp[cnt][j][0][1] ;
temp = f(cnt-1,digit[cnt]) ; // id * 1e(cnt-1)
int flag=0; //表示是否出现过13
for(i = cnt-1 ; i > 0 ; i--)
{
for(j = 0 ; j < digit[i] ; j++) //扫描0-id-1
{
for(k = 0 ; k < 13 ; k++) //扫描所有余数
{
//扫描的数是: temp + j....(j往后一共i位) 余数为k
if((temp+k)%13 != 0)
continue;
ans += dp[i][j][k][1] ; //+已经含有13,且当前余数k 的个数
if( ( digit[i+1] == 1&& j == 3) ) //如果这里出现了13
ans += dp[i][j][k][0] ; //那就可以+前面 没有出现过13 的
else if( flag )
ans += dp[i][j][k][0];
}
}
if( digit[i+1] == 1 && digit[i] == 3 )
flag = 1 ;
temp = temp + f(i-1,digit[i]) ; //temp 更新。 +当前id
}
return ans ;
}
int main()
{
ll n ;
init() ;
int i , j , k ;
while( scanf("%I64d", &n) != EOF )
{
printf("%I64d\n", solve(n+1) ) ;
}
return 0;
}
可以总结一下,这两题表明的这种做法,大概是这么个套路:
1.先预处理(init):把所有的满足条件的一些dp[i][j][k][l] 什么的都算出来,
2.然后再dp 统计一下(count),整个时间复杂度会很短。
codeforces 55D
这题题意:
条件: 一个数需要能够被组成他的每一位非0的数整除;
比如 abcd 要能够 abcd%a = abcd%b =abcd%c =abcd%d=0;
这题咋一想挺难的, 这个条件不给你明显的规则让你去 dp。所以我们需要去找规则,需要把已知的信息转换一下,转换成我们需要的。
比如abcd 要能够整除 a、b、c、d 那么也就是 %lcm(a,b,c,d)=0;
这一点很重要。 那加入一个数1-9去包括了,也就是lcm(1,2,3,4,5,6,7,8,9)=2520. 即我们最多要处理的余数也不过就是2520。
这个套路 的dp意义:dp[i][j][k] 就是 长度为i , (以j开头) 某个条件 ,(辅助条件)
这里我们第二个条件不需要用以j开头,因为 在dfs下 我们是一边dfs,一边就记录答案。为什么这样呢? 正是dfs 的回溯 属性,能够让我们在每一个dfs 下都能算出他后面的事情,自然也就能够得到这一层dfs 的贡献值。
另外一个很机智的点就是:
把0-2520 ,分为几段, 这里只能说Notonlysuccess 毕竟不是我等弱鸡能够看得懂的, 只能知道这么用确实会很好用,这个条件到底如何想出来,可能我还没精通到那个地步。
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
const double PI = acos(-1.0);
const double e = exp(1.0);
#define ll __int64
#define LL __int64
template<class T> T gcd(T a, T b) { return b ? gcd(b, a % b) : a; }
template<class T> T Lcm(T a, T b) { return a / gcd(a, b) * b; }
using namespace std;
/*
如果一个数s能被组成他的所有非0的数字:(a,b,c,d)整除,
也就意味着:
s % lcm(a,b,c,d)=0
lcm(1,2,3.....9)=2520,所以余数最大也就是这个数
2520又可以简化 为252 。
*/
ll dp[19][2521][55]; // 长度为 i ,余数为0-2520,前面有k个能整除2520 , 有多少个
int dig[20];
int index[2521]; //存前面所有数字 前面 有多少能整除252的数
int gcd(int a,int b) {
return b == 0 ? a : gcd(b , a % b);
}
ll dfs(int pos,int sum,int lcm, bool U) { //上一段累计下来的lcm,sum(%2520)。 U代表当前dig是否为原数字
if (pos == -1)
return sum % lcm == 0; //扫到最后一位,计算f(x)如果x满足则还可以+1
if (!U && dp[pos][sum][index[lcm]] != -1) //如果当前需要处理的东西已经有答案了
return dp[pos][sum][index[lcm]]; //相当于记忆化搜索
ll ret = 0; //没搜过,那我们就来算一下这:长度为pos,上一段遗留下来的sum,lcm 下面能有多少个满足的数
int end = U ? dig[pos] : 9; //若U为真,只遍历到dig[pos],否则0-9都遍历
/*
这一步是为什么呢? 比如19.....
第一位的时候,我们只算0.....
第二位的时候,我们只算0..... 1.... 2.... 3..... 4..... 5..... 6.... 7..... 8....
原理就和初始化做是差不多的,这也是为什么后来的U为真时 不更新dp;
*/
for (int i = 0 ; i <= end ; i ++) {
int Nlcm = lcm; //之前的lcm
if (i)
Nlcm = lcm / gcd(lcm , i) * i; //lcm=a*b/gcd, Nlcm:当前lcm
int Nsum = sum * 10 + i; //更新sum
if (pos) //扫到最后一位了
Nsum %= 252;
ret += dfs(pos - 1 , Nsum , Nlcm, U && i == end); //统计下面的有多少个满足这个条件
}
if (!U) //这个数本身 不+ 上来
dp[pos][sum][index[lcm]] = ret;
return ret;
}
ll func(ll num) { //实质就是直接去计算0-n, 可是想想预处理 感觉又不行,所以就用记忆化搜索,类似预处理!
int n = 0;
while (num) {
dig[n++] = num%10; //dig 从0开始 存低位
num /= 10;
}
return dfs(n - 1 , 0 , 1 , 1); //长度
}
void init() {
int sz = 0;
for (int i = 1 ; i <= 2520 ; i ++)
if (2520%i == 0)
index[i] = sz ++; // 就表示i前面有几个能整除252的数
memset(dp , -1 , sizeof(dp));
}
int main() {
init();
int T;
cin >> T;
while (T --) {
ll a , b;
cin >> a >> b;
cout << func(b) - func(a - 1) << endl;
}
return 0;
}
dfs 套路的马上实践题:
ICPC 沈阳网络赛的题
hdu 5989
条件: 奇数段的长度为偶数,并且偶数段的长度为奇数。
比如:11222 、 357924
当时 按着这个套路慢慢写,确实debug了一会,因为那个 pos=-1的情况 ,有几种情况我都是特判的。
还有一个v+1我写成了v++ 也是醉了
但是实践证明 这个写法确实是有迹可循,可以模仿!
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
const double PI = acos(-1.0);
const double e = exp(1.0);
#define ll __int64
#define LL __int64
template<class T> T gcd(T a, T b) { return b ? gcd(b, a % b) : a; }
template<class T> T Lcm(T a, T b) { return a / gcd(a, b) * b; }
using namespace std;
ll dp[20][10][20]; //长度为i 的,前面留过来的数 为奇/偶,且长度为k
int dig[20];
ll dfs(int pos,int v,int flag,bool U){ //flag代表从上一位的是奇数还是偶数,v代表长度
// printf("find %d %d %d %d ans=%d\n",pos,v,flag,U,flag%2!= v%2);
if(pos==-1){
if(flag==0 && v==0){
return 1;
}
if(flag%2== v%2)
return 0;
else
return 1;
}
if(!U && dp[pos][flag][v]!=-1)
return dp[pos][flag][v];
//下面都是没有搜过的
ll ret=0;
int end=U?dig[pos]:9;
for(int i=0;i<=end;i++){
if(v==0){
if(i) ret+=dfs(pos-1,1,i,i==end && U);
else ret+=dfs(pos-1,0,i,i==end && U);
continue;
}
if(i&1){ //如果这一位是奇数
if(flag%2==1){ //如果上一位是奇数
// v++;// debug 出要不能这么写
ret+=dfs(pos-1,v+1,i,i==end && U);
}
else{
if(v&1){
ret+=dfs(pos-1,1,i,i==end && U);
}
}
}
else{//,这一位是偶数
if(flag%2){ //如果上一位是奇数
if(v%2==0){
// printf("%d %d %d\n",i,flag,v);
ret+=dfs(pos-1,1,i,i==end && U);
}
}
else{
ret+=dfs(pos-1,v+1,i,i==end && U);
}
}
}
if(!U)
dp[pos][flag][v]=ret;
return ret;
}
ll func(ll num) {
int n = 0;
while (num) {
dig[n++] = num%10; //dig 从0开始 存低位
num /= 10;
}
if(dig[n-1]%2)
return dfs(n-1,0,0,1);
else
return dfs(n-1,0,0,1);
}
void init(){
memset(dp,-1,sizeof(dp));
}
int main() {
init();
int T;
int cas=1;
scanf("%d",&T);
while(T--){
ll l,r;
scanf("%I64d %I64d",&l,&r);
printf("Case #%d: ",cas++); printf("%I64d\n",func(r)-func(l-1));
}
return 0;
}
2016 11-15补题
bzoj1833
纯手写入门数位dp
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
const double PI = acos(-1.0);
const double e = exp(1.0);
#define LL long long
using namespace std;
/*
* 读错了一遍题。。。。 不是出现带1的数字有多少个,是1出现了多少次,比如11 就算1 出现了两次
*
*
* */
//LL dp[13][15][15][2];
map< LL,map<LL,map<LL,map<LL,LL> > > >dp;
int dig[15];
LL dfs(int pos,LL flag,int x,int u,bool cnt){ //cnt代表 前面至少已经出现过一个>0的数
// printf("%d %d %d %d %d \n",pos,flag,x,u,cnt);
if(pos==-1){
if(flag>0)
return flag;
if(!cnt && !x) //就是找0的情况 0也算出现了一次0
return 1;
return 0;
}
if( !u && dp[pos][flag][x][cnt]){
return dp[pos][flag][x][cnt];
}
LL ans=0;
int end=u?dig[pos]:9;
for(int i=0;i<=end;i++){ //假设这一位是i
if(flag){
if(i==x) ans+=dfs(pos-1,flag+1,x,(i==end&&u),1);
else ans+=dfs(pos-1,flag,x,(i==end&&u),1);
}
else{
if(i==x){
if(x==0 && !cnt)
ans+=dfs(pos-1,flag,x,(i==end&&u),0); //如果前面没有出现过大于0的数,且要找0
else
ans+=dfs(pos-1,(LL)flag+1,x,(i==end&&u),1);
}
else{
if(i>0) ans += dfs(pos - 1, flag, x, (i == end && u), 1);
else ans += dfs(pos - 1, flag, x, (i == end && u), cnt);
}
}
}
if(!u)
dp[pos][flag][x][cnt]=ans;
// printf("%d %d %d %d %d = %I64d,%d\n",pos,flag,x,u,cnt, dp[pos][flag][x][cnt],ans);
return ans;
}
LL func(LL num,int x) {
int n = 0;
while (num) {
dig[n++] = num%10; //dig 从0开始 存低位
num /= 10;
}
if(n==0){
if(x==0)
return 1;
return 0;
}
return dfs(n-1,0,x,1,0);
}
int main(){
freopen("1.txt","r",stdin);
LL a,b;
cin>>a>>b;
cout<<(func(b,0)-func(a-1,0));
for(int i=1;i<=9;i++){
printf(" ");
cout<< (func(b,i)-func(a-1,i));
}
printf("\n");
}