动态规划10_数位DP1
2015-04-17 08:56
369 查看
文章来源:刘聪《浅谈数位类统计问题》
在信息学竞赛中,有这样一类问题:求给定区间中,满足给定条件的某个D 进制数或 此类数的数量。所求的限定条件往往与数位有关,例如数位之和、指定数码个数、数的大小 顺序分组等等。题目给定的区间往往很大,无法采用朴素的方法求解。此时,我们就需要利用数位的性质,设计 log(n)级别复杂度的算法。解决这类问题最基本的思想就是“逐位确定”
的方法。下面就让我们通过几道例题来具体了解一下这类问题及其思考方法。
【例题 1】Amount of degrees (ural 1057)
题目链接:ural 1057
题目大意:
求给定区间[X,Y]中满足下列条件的整数个数:这个数恰好等于K 个互不相等的 B 的整 数次幂之和。
分析: 所求的数为互不相等的幂之和,亦即其B 进制表示的各位数字都只能是 0和 1。因此, 我们只需讨论二进制的情况,
其他进制都可以转化为二进制求解。 很显然,数据范围较大,不可能采用枚举法,算法复杂度必须是 log(n)级别,
因此我们 要从数位上下手。
本题区间满足区间减法,因此可以进一步简化问题:令 count[i..j]表示[i..j]区间内合法数 的个数,
则count[i..j]=count[0..j]-count[0..i-1]。换句话说,给定 n,我们只需求出从0 到 n 有多少个符合条件的数。
假设 n=13,其二进制表示为 1101,K=3。我们的目标是求出 0 到 13 中二进制表示含 3 个 1 的数的个数。为了方便思考,让我们画出一棵高度为 4 的完全二叉树:
为了方便起见,树的根用 0 表示。这样,这棵高度为4 的完全二叉树就可以表示所有 4 位二进制数(0..2^4-1) ,每一个叶子节点代表一个数。其中,红色路径表示 n。所有小于 n的 数组成了三棵子树,分别用蓝色、绿色、紫色表示。因此,统计小于 13 的数,就只需统计 这三棵完整的完全二叉树:统计蓝子树内含 3 个 1 的数的个数、统计绿子树内含 2 个1 的数 的个数(因为从根到此处的路径上已经有 1 个 1),以及统计紫子树内含 1个 1
的数的个数。
注意到,只要是高度相同的子树统计结果一定相同。而需要统计的子树都是“右转”时遇到 的。当然,我们不能忘记统计n 本身。
实际上,在算法最初时将 n 自加 1,可以避免讨论 n 本身,但是需要注意防止上溢。 剩下的问题就是,如何统计一棵高度为 i的完全二叉树内二进制表示中恰好含有 j 个1 的数的个数。这很容易用递推求出:
设 f[i,j]表示所求,则分别统计左右子树内符合条件数的 个数,有 f[i,j]=f[i-1,j]+f[i-1,j-1]。 这样,我们就得出了询问的算法:首先预处理 f,然后对于输入 n,我们在假想的完全 二叉树中,从根走到 n所在的叶子,每次向右转时统计左子树内数的个数。
下面是 C++代码:
最后的问题就是如何处理非二进制。对于询问 n,我们需要求出不超过 n 的最大 B 进制 表示只含 0、1的数:
找到n 的左起第一位非 0、1 的数位,将它变为 1,并将右面所有数位 设为 1。将得到的 B 进制表示视为二进制进行询问即可。
预处理递推 f的时间复杂度为O(log2(n)),共有 O(log(n))次查询,因此总时间复杂度为 O(log(n)*log(n))。
实际上,最终的代码并不涉及树的操作,我们只是利用图形的方式来方便思考。因此也 可以只从数位的角度考虑:对于询问 n,我们找到一个等于 1 的数位,将它赋为 0,则它右 面的数位可以任意取,我们需要统计其中恰好含有 K-tot 个 1 的数的个数(其中 tot 表示这 一位左边的1 的个数) ,则可以利用组合数公式求解。逐位枚举所有”1”进行统计即可。 我们发现,之前推出的
f 正是组合数。同样是采用“逐位确定”的方法,两种方法异曲 同工。当你觉得单纯从数位的角度较难思考时,不妨画出图形以方便思考。
最终代码:
【例题 2】Sorted bit sequence (spoj 1182)
题目链接:spoj 1182
题目大意:
将区间[m,n]内的所有整数按照其二进制表示中 1 的数量从小到大排序。如果 1 的数量 相同,则按照数的大小排序。
求这个序列中的第 k个数。其中,负数使用补码来表示:一个 负数的二进制表示与其相反数的二进制之和恰好等于2^32。
例如,当 m=0,n=5 时,排序后的序列如下:
当 m=-5,n=-2 时,排序后的序列如下:
输入:包含多组测试数据。第一行是一个不超过 1000 的正整数,表示测试数据数量。 每组数据包含 m,n,k三个整数。
输出:对于每组数据,输出排序后的序列中第 k个数。
数据规模:m × n ≥ 0, -2^31 ≤ m ≤ n ≤ 2^31-1 ,1 ≤ k ≤ min{n − m + 1, 2 147 473 547}。
分析: 我们首先考虑 m、n同正的情况。 由于排序的第一关键字是 1 的数量,第二关键字是数的大小,
因此我们很容易确定答案 中 1 的个数:依次统计区间[m,n]内二进制表示中含 1的数量为 0,1,2,…的数,直到累加的答 案超过 k,
则当前值就是答案含 1 的个数,假设是 s。利用例一的算法可以解决这个问题。 同时,我们也求出了答案是第几个[m,n]中含 s个 1 的数。因此,只需二分答案,求出[m,ans] 中含 s 个 1 的数的个数进行判断即可。
由于每次询问的复杂度为 O(log(n)),故二分的复杂度为 O(log2(n)),这同时也是预处理 的复杂度,因此此算法较为理想。
m<0 的情况,也不难处理,我们只要忽略所有数的最高位,求出答案后再将最高位赋回 1 即可。或者也可以直接将负数视为 32位无符号数,采用同正数一样的处理方法。两种方 法都需要特别处理 n=0 的情况.
最终代码:
动态规划10_数位DP2会继续讲解例题。。。
在信息学竞赛中,有这样一类问题:求给定区间中,满足给定条件的某个D 进制数或 此类数的数量。所求的限定条件往往与数位有关,例如数位之和、指定数码个数、数的大小 顺序分组等等。题目给定的区间往往很大,无法采用朴素的方法求解。此时,我们就需要利用数位的性质,设计 log(n)级别复杂度的算法。解决这类问题最基本的思想就是“逐位确定”
的方法。下面就让我们通过几道例题来具体了解一下这类问题及其思考方法。
【例题 1】Amount of degrees (ural 1057)
题目链接:ural 1057
题目大意:
求给定区间[X,Y]中满足下列条件的整数个数:这个数恰好等于K 个互不相等的 B 的整 数次幂之和。
分析: 所求的数为互不相等的幂之和,亦即其B 进制表示的各位数字都只能是 0和 1。因此, 我们只需讨论二进制的情况,
其他进制都可以转化为二进制求解。 很显然,数据范围较大,不可能采用枚举法,算法复杂度必须是 log(n)级别,
因此我们 要从数位上下手。
本题区间满足区间减法,因此可以进一步简化问题:令 count[i..j]表示[i..j]区间内合法数 的个数,
则count[i..j]=count[0..j]-count[0..i-1]。换句话说,给定 n,我们只需求出从0 到 n 有多少个符合条件的数。
假设 n=13,其二进制表示为 1101,K=3。我们的目标是求出 0 到 13 中二进制表示含 3 个 1 的数的个数。为了方便思考,让我们画出一棵高度为 4 的完全二叉树:
为了方便起见,树的根用 0 表示。这样,这棵高度为4 的完全二叉树就可以表示所有 4 位二进制数(0..2^4-1) ,每一个叶子节点代表一个数。其中,红色路径表示 n。所有小于 n的 数组成了三棵子树,分别用蓝色、绿色、紫色表示。因此,统计小于 13 的数,就只需统计 这三棵完整的完全二叉树:统计蓝子树内含 3 个 1 的数的个数、统计绿子树内含 2 个1 的数 的个数(因为从根到此处的路径上已经有 1 个 1),以及统计紫子树内含 1个 1
的数的个数。
注意到,只要是高度相同的子树统计结果一定相同。而需要统计的子树都是“右转”时遇到 的。当然,我们不能忘记统计n 本身。
实际上,在算法最初时将 n 自加 1,可以避免讨论 n 本身,但是需要注意防止上溢。 剩下的问题就是,如何统计一棵高度为 i的完全二叉树内二进制表示中恰好含有 j 个1 的数的个数。这很容易用递推求出:
设 f[i,j]表示所求,则分别统计左右子树内符合条件数的 个数,有 f[i,j]=f[i-1,j]+f[i-1,j-1]。 这样,我们就得出了询问的算法:首先预处理 f,然后对于输入 n,我们在假想的完全 二叉树中,从根走到 n所在的叶子,每次向右转时统计左子树内数的个数。
下面是 C++代码:
void init() { f[0][0]=1; for(int i=1;i<=31;++i) { f[i][0]=f[i-1][0]; for(int j=1;j<=i;++j) f[i][j]=f[i-1][j]+f[i-1][j-1]; } } int calc(int x,int k) //统计[0...x]内二进制表示含有k个1的数的个数 { int tot=0,ans=0;//tot记录当前路径上已有1的数量,ans代表答案 for(int i=31;i>0;i--) { if(x&(1<<i)) { tot++; if(tot>k) break; x=x^(1<<i); } if((1<<(i-1))<=x) ans+=f[i-1][k-tot]; } if(tot+x==k) ans++; return ans; }
最后的问题就是如何处理非二进制。对于询问 n,我们需要求出不超过 n 的最大 B 进制 表示只含 0、1的数:
找到n 的左起第一位非 0、1 的数位,将它变为 1,并将右面所有数位 设为 1。将得到的 B 进制表示视为二进制进行询问即可。
预处理递推 f的时间复杂度为O(log2(n)),共有 O(log(n))次查询,因此总时间复杂度为 O(log(n)*log(n))。
实际上,最终的代码并不涉及树的操作,我们只是利用图形的方式来方便思考。因此也 可以只从数位的角度考虑:对于询问 n,我们找到一个等于 1 的数位,将它赋为 0,则它右 面的数位可以任意取,我们需要统计其中恰好含有 K-tot 个 1 的数的个数(其中 tot 表示这 一位左边的1 的个数) ,则可以利用组合数公式求解。逐位枚举所有”1”进行统计即可。 我们发现,之前推出的
f 正是组合数。同样是采用“逐位确定”的方法,两种方法异曲 同工。当你觉得单纯从数位的角度较难思考时,不妨画出图形以方便思考。
最终代码:
//题目: ural 1057
//时间: 2015-04-17
//作者: Miki
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<string>
using namespace std;
int f[32][32];
//将B进制转化为二进制
int change(int x,int b)
{
string s;
do
{
s=char('0'+x%b)+s;
x/=b;
} while (x>0);
for(int i=0;i<s.size();++i)
if (s[i]>'1')
{
for (int j=i;j<s.size();++j)
s[j]='1';
break;
}
x=0;
for(int i=0;i<s.size();++i)
x=x|((s[s.size()-i-1]-'0')<<i);
return x;
}
void init() { f[0][0]=1; for(int i=1;i<=31;++i) { f[i][0]=f[i-1][0]; for(int j=1;j<=i;++j) f[i][j]=f[i-1][j]+f[i-1][j-1]; } } int calc(int x,int k) //统计[0...x]内二进制表示含有k个1的数的个数 { int tot=0,ans=0;//tot记录当前路径上已有1的数量,ans代表答案 for(int i=31;i>0;i--) { if(x&(1<<i)) { tot++; if(tot>k) break; x=x^(1<<i); } if((1<<(i-1))<=x) ans+=f[i-1][k-tot]; } if(tot+x==k) ans++; return ans; }
int main()
{
// freopen("in.txt","r",stdin);
init();
int x,y,b,k;
while(scanf("%d %d %d %d",&x,&y,&k,&b)!=EO
4000
F)
{
x=change(x,b);y=change(y,b);
printf("%d\n",calc(y,k)-calc(x-1,k));
}
return 0;
}
【例题 2】Sorted bit sequence (spoj 1182)
题目链接:spoj 1182
题目大意:
将区间[m,n]内的所有整数按照其二进制表示中 1 的数量从小到大排序。如果 1 的数量 相同,则按照数的大小排序。
求这个序列中的第 k个数。其中,负数使用补码来表示:一个 负数的二进制表示与其相反数的二进制之和恰好等于2^32。
例如,当 m=0,n=5 时,排序后的序列如下:
当 m=-5,n=-2 时,排序后的序列如下:
输入:包含多组测试数据。第一行是一个不超过 1000 的正整数,表示测试数据数量。 每组数据包含 m,n,k三个整数。
输出:对于每组数据,输出排序后的序列中第 k个数。
数据规模:m × n ≥ 0, -2^31 ≤ m ≤ n ≤ 2^31-1 ,1 ≤ k ≤ min{n − m + 1, 2 147 473 547}。
分析: 我们首先考虑 m、n同正的情况。 由于排序的第一关键字是 1 的数量,第二关键字是数的大小,
因此我们很容易确定答案 中 1 的个数:依次统计区间[m,n]内二进制表示中含 1的数量为 0,1,2,…的数,直到累加的答 案超过 k,
则当前值就是答案含 1 的个数,假设是 s。利用例一的算法可以解决这个问题。 同时,我们也求出了答案是第几个[m,n]中含 s个 1 的数。因此,只需二分答案,求出[m,ans] 中含 s 个 1 的数的个数进行判断即可。
由于每次询问的复杂度为 O(log(n)),故二分的复杂度为 O(log2(n)),这同时也是预处理 的复杂度,因此此算法较为理想。
m<0 的情况,也不难处理,我们只要忽略所有数的最高位,求出答案后再将最高位赋回 1 即可。或者也可以直接将负数视为 32位无符号数,采用同正数一样的处理方法。两种方 法都需要特别处理 n=0 的情况.
最终代码:
//题目:spoj 1182 //时间:2015-04-17 //作者:Miki #include<iostream> #include<string> using namespace std; int f[32][32]; void init() { f[0][0]=1; for (int i=1;i<=31;++i) { f[i][0]=f[i-1][0]; for (int j=1;j<=i;++j) f[i][j]=f[i-1][j]+f[i-1][j-1]; } } int calc(int x,int k) { if (x<0) return 0; int tot=0,ans=0; for (int i=31;i>0;--i) { if (x&(1U<<i)) { tot++; if (tot>k) break; x=x^(1U<<i); } if ((1<<(i-1))<=x) { ans+=f[i-1][k-tot]; } } if (tot+x==k) ans++; return ans; } int find(int x,int y,int k) { int i; for (i=0;;i++) { int t=calc(y,i)-calc(x-1,i); if (k<=t) break; k-=t; } if (calc(x,i)-calc(x-1,i)==k) return x; unsigned l=x,r=y,tmp=calc(x-1,i); while (l+1<r) { int mid=(l+r)/2; int t=calc(mid,i)-tmp; if (t<k) l=mid; else r=mid; } return l+1; } int main() { // freopen("in.txt","r",stdin); init(); int T; scanf("%d",&T); while (T--) { int x,y,k; bool negative=false; scanf("%d%d%d",&x,&y,&k); if (y==0) { if (k==1) { cout<<0<<endl; continue; } y--,k--;//减去0 } if (x<0) //负数转化为正数 negative为true x^=1<<31,y^=1<<31,negative=true; int ans=find(x,y,k); if (negative)// negative为true 将负数还原 ans^=1<<31; cout<<ans<<endl; } }
动态规划10_数位DP2会继续讲解例题。。。
相关文章推荐
- 动态规划晋级——HDU 3555 Bomb【数位DP详解】
- 动态规划——数位dp入门(一)
- 有关动态规划(主要是数位DP)的一点讨论
- 动态规划晋级——HDU 3555 Bomb【数位DP详解】
- 动态规划——数位dp入门(二)
- HDU 5691(动态规划-状压dp)
- leetcode 213. House Robber II 入室抢劫 抢劫问题 + 一道经典的DP动态规划问题
- 动态规划之背包DP专题
- 动态规划问题-DP 最大子段和O(n)解决方法
- POJ1141 Brackets Sequence (dp动态规划,递归)
- Bookshelf题解动态规划DP
- 大坑!动态规划!SHU 1149 能量项链(区间DP)
- 动态规划(DP),递推,最大子段和,POJ(2479,2593)
- DP动态规划——hdu 1008 Common Subsequence(最长公共子序列)
- 动态规划10:变态跳台阶
- DP动态规划介绍
- HDU 4628(动态规划-状压dp)
- 动态规划问题_dp
- 动态规划、贪心、dynamic programming(DP)
- [原]POJ1141 Brackets Sequence (dp动态规划,递归)