您的位置:首页 > 其它

动态规划10_数位DP2

2015-04-18 00:40 1066 查看
文章来源:刘聪《浅谈数位类统计问题》

【例题 3】Sequence (spoj 2319) 

题目链接:SPOJ2319

题目大意: 

给定所有K 位二进制数:0,1,…,2^K-1。你需要将它们分成恰好 M组,每组都是原序列 中连续的一些数。

设 Si(1 ≤ i ≤ M)表示第 i组中所有数的二进制表示中 1 的个数,S 等于所有 Si中的最大值。你的任务是令 S 最小。

 输入:两个整数 K 和 M。

 输出:一个整数表示 S 的最小值。答案不保证可用 64 位整数储存。 

数据规模:1 ≤ K ≤ 100, 1 ≤ M ≤ 100, M ≤ 2^K。 

分析: 

直接做不容易做,我们可以通过二分答案来处理。

显然,假如我们知道了 S 的值,可以贪心求出序列最少被分为几组。

当 S变大时,分组 数量是非增的,根据这个单调性,我们二分 S,然后贪心求出[0,2K-1]的区间最少被划分的组 数进行判断 

剩下的问题就是如何求出组数。实际上这非常简单,由于 M 较小,因此我们每次从当 前起点 a 开始找到最大的 b

 满足 count(b)-count(a-1)≤S(其中 count(i)表示 0..i 的所有整数的 二进制中 1的数量),再以 b+1 为起点继续寻找,

直到某次找到的 b 超过2^K-1,或者已有区 间数量超过M。 

利用逐位确定的方法,我们很容易求出 count(i):类似例一,在高度为 K 的完全二叉树 中,从根走到叶子 i,

右转的时候累加左子树的权值。也就是找到 i 的所有二进制为1 的数 位,计算其权值。其中要注意累加之前已有的 1的数量。

 寻找 b 的方法也不难:设 find(i)表示最小的k 满足 0..k 的数的二进制表示中 1 的数量不超过 i 的最大k,则 b=find(count(a-1)+s)。

find(i)也使用逐位确定的方法求解,在树中从根向 叶子行走,若已有权值加上当前节点左子树的权值不超过 i 则向右走,否则向左走。 

count(i)和 find(i)的复杂度都是O(K),因此算法总复杂度为 O(MKlog(S))加上高精度的复杂度。 

最终代码:

#include<iostream>
#include<cstring>
using namespace std;
const int MXK=101;
const int D=10000;
class bignum
{
public:
bignum();
bignum(int);
friend bignum operator+(const bignum&,const bignum&);
friend bignum operator-(const bignum&,const bignum&);
friend bignum operator*(const bignum&,int);
friend bool operator<=(const bignum&,const bignum&);
friend bignum div2(const bignum&);
void print();
private:
int s[10],l;
};
bignum::bignum()
{
memset(s,0,sizeof(s));
l=1;
}
bignum::bignum(int a)
{
memset(s,0,sizeof(s));
l=1;
s[0]=a;
while (s[l-1]>=D)
{
s[l]=s[l-1]/D;
s[l-1]%=D;
l++;
}
}
bignum operator+(const bignum& a,const bignum& b)
{
bignum c;
c.l=a.l<b.l?b.l:a.l;
int i,x=0;
for (i=0;i<c.l;++i)
{
x=x/D+a.s[i]+b.s[i];
c.s[i]=x%D;
}
while (x>=D)
{
x/=D;
c.s[c.l++]=x%D;
}
return c;
}
bignum operator-(const bignum& a,const bignum& b)
{
bignum c=a;
int i;
for (i=0;i<c.l;++i)
{
c.s[i]-=b.s[i];
if (c.s[i]<0)
{
c.s[i]+=D;
c.s[i+1]--;
}
}
while (c.l>1 && c.s[c.l-1]==0)
c.l--;
return c;
}
bignum operator*(const bignum& a,int b)
{
bignum c;
c.l=a.l;
int i,x=0;
for (i=0;i<c.l;++i)
{
x=x/D+a.s[i]*b;
c.s[i]=x%D;
}
while (x>=D)
{
x/=D;
c.s[c.l++]=x%D;
}
return c;
}
bool operator<=(const bignum& a,const bignum& b)
{
if (a.l!=b.l) return a.l<b.l;
int i;
for (i=a.l-1;i>=0;--i)
if (a.s[i]!=b.s[i])
return a.s[i]<b.s[i];
return true;
}
// a / 2 的结果
bignum div2(const bignum& a)
{
int i;
bignum b=a;
for (i=b.l-1;i>=0;i--)
{
if (b.s[i]&1)
{
b.s[i]--;
if (i>0)
b.s[i-1]+=D;
}
b.s[i]/=2;
}
while (b.l>1 && b.s[b.l-1]==0)
b.l--;
return b;
}
void bignum::print()
{
printf("%d",s[l-1]);
for (int i=l-2;i>=0;--i)
printf("%04d",s[i]);
printf("\n");
}

int k,m;
bignum f[MXK],p[MXK];
// f[i]表示高度为i+1,根节点为0的完全二叉树的1的个数
// p[i]的值为2^i , 用来计算高度为i, 根节点为1时,
// 根节点的1出现的次数,显然会被计算2^i次
bignum count(bignum n) //统计1-n中所有数有多少个1
{
int i,cur=0;
bignum ans=0;
//由题意知,一个数最多有k个1
for (i=k;i>0;i--)
{
if (p[i]<=n)
{
n=n-p[i];
++cur;
}
if (p[i-1]<=n)
{
ans=ans+f[i-1]+p[i-1]*cur;
}
}
// ans=ans+f[0]+p[0]*cur
ans=ans+cur;
// n==1
if (n<=1 && 1<=n)
ans=ans+1;
return ans;
}
bignum calc(bignum s)//统计1-?中的1的个数不超过s的最大?
{
bignum a=0;
int i,cur=0;
for (i=k;i>0;i--)
{
if (f[i-1]+p[i-1]*cur<=s)
{
a=a+p[i-1];
s=s-(f[i-1]+p[i-1]*cur);
++cur;
}
}
if (cur<=s)
a=a+1;
return a-1;
}
bool check(bignum s)
{
bignum a=1;
int cnt=0;
// a < = p[k]-1 (2^k-1)
while (a+1<=p[k])
{
bignum b=calc(count(a-1)+s);
if (++cnt>m)
return false;
a=b+1;
}
return true;
}
int main()
{
// freopen("in.txt","r",stdin);
while(scanf("%d %d",&k,&m)!=EOF)
{
p[0]=1;
for (int i=1;i<=k;++i)
p[i]=p[i-1]+p[i-1];

f[0]=0;
for (int i=1;i<=k;++i)
f[i]=f[i-1]+f[i-1]+p[i-1];
// r 为 0....(2^k)-1 二进制表示 1 的总个数
bignum l=0,r=count(p[k]-1);
while (l+2<=r)
{
bignum mid=div2(l+r);
if (check(mid))
r=mid;
else
l=mid;
}
r.print();
}
return 0;
}

【例题 4】Tickets (sgu 390) 

题目大意: 

有一位售票员给乘客售票。对于每位乘客,他会卖出多张连续的票,直到已卖出的票的 编号的数位之和不小于给定的正数 k。

然后他会按照相同的规则给下一位乘客售票。初始时, 售票员持有的票的编号是从 L到 R 的连续整数。

请你求出,售票员可以售票给多少位乘客。

输入:三个整数 L,R 和 k。 

输出:输出一个整数,表示问题的答案。

数据规模:1 ≤ L ≤ R ≤ 1018,1 ≤ k ≤ 1000。 

分析: 

本题与例 3有些类似,也是对区间内的整数进行连续的分组。与例 3不同的是,本题中 k 的值较小,

也就表示分组数量非常巨大,不可能求出每一个分组。 

让我们同之前一样,从特殊情况开始考虑。假设给定的区间是[1..10^h-1](也就是一棵完 整的高度为h 的完全十叉树),

考虑如何由子问题组合出原问题。这里我们遇到的问题是, 两棵子树间的“合并”较难处理:前一棵子树的最后一个区间未必

是满的。亦即,一棵子树 的头几个元素会并入前一棵子树最后的分组当中。为了解决这个问题,我们引入一维,

记录 每棵子树之前有多少空间。因此就得到了下面的递推算法:

  设 f[h,sum,rem]表示对于高度为 h的完全十叉树,在这个区间内的每个数需要累加的数 字和为 sum,在这个区间之前有 rem的

空间,所能得出的分组数量。f 的返回值需要记录两 个数:f[h,sum,rem].a 记录分组数量,f[h,sum,rem].b 记录其最后一个小组

剩余的空间,以便 于与下一个子树合并。f 可由其十个子树推出:  

 for i:=0 to 9 do f[h,sum,rem]:=merge(f[h,sum,rem],f[h-1,sum+i,f[h,sum,rem].b]); 

其中,merge 函数表示合并两个记录,它的伪代码是: 

function merge(x,y);  

 x.a:=x.a+y.a;   

x.b:=y.b;   

return x; 

当h=0 时,若 rem>0,则 f[h,sum,rem].a=0,否则 f[h,sum,rem]=1;b 需要根据 sum和 k 的关系求出。 

这样,我们就求出了完整子树的分组数量。接下来的工作就比较容易了:在完全十叉树 中,我们从叶子 L走到叶子 R,计算途中遇到的完整子树并加以合并即可。遍历的时候,从 L所在的叶子一直向上走直到 L和 R 的最近公共祖先的下一层,途中计算路径右侧的兄弟 子树。然后在 LCA 的下一层,从 L 的祖先走到 R的祖先,途中计算经过的子树。最后,向 下走到 R,途中计算左侧的兄弟子树。当然,不能忘记统计
L和 R 所在的叶子。

 以下图为例:(为了方便起见这里使用二叉树,思想和十叉树是一致的)



我们首先求出 L和 R 的最近公共祖先(在这里就是根节点),然后从 L所在的叶子向上 走,每走一步就计算所有右侧的兄弟子树(图中两个绿色子树) 。走到 LCA 的下一层后,再 向右走到 R 的祖先。然后向下走到 R 所在叶子,计算途径的左侧兄弟子树

( 两 个 蓝色子树)。

 需要特殊处理的是 L=R 的情况,以及 R=1018的情况(如果只计算到 18 层) 。另外,如 果最后一个分组未满则不计入总组数,

需要特别注意。

递推求 f 的复杂度为O(klog2(R)),需要询问的子树数量是 O(log(R)),因此总复杂度是递 推的 O(klog2(R))。

递推建议采用记忆化搜索实现。  

最终代码:

#include<iostream>
#include<cstring>
using namespace std;
const long long  MXR=1e18;
struct ret
{
long long a;
int b;
ret operator+=(ret y)//合并操作,为了方便进行了重载
{
a+=y.a;
b=y.b;
return *this;
}
};
long long ten[19]={1};
ret f[19][170][1000];
bool vis[19][170][1000];
int k;

// 对一棵高度为h,所有元素的数位和需要额外+sum,在此树之前的小组
// 有rem的空间的完全十叉树进行记忆化搜索
ret calc(int h,int sum,int rem)
{
if (vis[h][sum][rem]) return f[h][sum][rem];
vis[h][sum][rem]=true;
ret &ans=f[h][sum][rem];
if (h==0) //h=0的情况
{
if (rem==0)
{
ans.a=1;
if (sum<k) ans.b=k-sum;
else ans.b=0;
}
else
{
ans.a=0;
if (rem>sum)
ans.b=rem-sum;
else ans.b=0;
}
return ans;
}
ans.a=0;ans.b=rem;
int i;
for (i=0;i<=9;++i) //h>0时,根据十个儿子合并得出
{
ans+=calc(h-1,sum+i,ans.b);
}
return ans;
}
ret count(long long l,long long r)
{
long long l2=l,r2=r;
int i,j,h=-1,suml=0,sumr=0;
for (i=18;i>=0;--i) //求出其LCA、两个数分别的数位和
{
if (h<0 && l/ten[i]!=r/ten[i])
h=i;
suml+=l/ten[i];
sumr+=r/ten[i];
l%=ten[i];r%=ten[i];
}
ret ans;
if (h<0) //l=r的情况
{
ans.a=1;
if (suml>=k)
ans.b=0;
else
ans.b=k-suml;
return ans;
}
ans.a=0;ans.b=0;
l=l2,r=r2;
ans+=calc(0,suml,ans.b);//不要忘记l
for (i=0;i<h;++i) //向上走
{
suml-=l%ten[i+1]/ten[i];
for (j=l%ten[i+1]/ten[i]+1;j<=9;++j)
ans+=calc(i,suml+j,ans.b);
}
suml-=l%ten[h+1]/ten[h];
for (j=l%ten[h+1]/ten[h]+1;j<r%ten[h+1]/ten[h];++j)
ans+=calc(h,suml+j,ans.b);//在LCA的下一层,向右走

for (i=h-1;i>=0;--i) //向下走
{
suml+=r%ten[i+2]/ten[i+1];
for (j=0;j<r%ten[i+1]/ten[i];++j)
ans+=calc(i,suml+j,ans.b);
}
ans+=calc(0,suml+r%10,ans.b);//不要忘记r
return ans;
}
int main()
{
// freopen("in.txt","r",stdin);
long long l,r;
for (int i=1;i<=18;++i)
ten[i]=10*ten[i-1];
while(scanf("%lld %lld %lld",&l,&r,&k)!=EOF)
{
bool flag=false;

if (r==MXR && l<r)
{
r--;
flag=true;
}
ret ans=count(l,r);
if (flag)
{
if (ans.b==0)
{
++ans.a;
ans.b=k;
}
ans.b--;
}
if (ans.b>0)
ans.a--;
printf("%lld\n",ans);
}
}


【例题 5】Graduated Lexicographical Ordering (zju 2599) 

题目大意: 

考虑所有从1 到 n的整数。我们设一个整数 x 的十进制各位数字之和为 w(x)。 

让我们重新将整数排序:对于整数 a 和整数 b,如果w(a)<w(b),则 a 在 b 之前。如果 w(a)=w(b),那么a 在b 之前当且仅当

a 的十进制表示在字典序中小于 b 的十进制表示。

例 如: 120 < grlex4 因为 w(120) = 1 + 2 + 0 = 3 < 4 = w(4)。 555 < grlex78 因为 w(555) = 15 = w(78) 并且 "555" 的字典序

小于 "78"。 20 < grlex200 因为 w(20) = 2 = w(200) 并且 "20" 的字典序小于 "200"。 

给定整数 n和 k,你需要求出 1-n中的数按照上述方法排序后,k 是第几个数,以及第 k 个数是多少。

输入:包含多组数据,每组数据包含两个整数 n和 k。输入的最后一行是 n=k=0。 

输出:对于每组数据,输出一行两个整数,分别表示k在 1-n中的编号,以及第 k个数 是多少。

数据规模:1 ≤ k ≤ n ≤ 10^18。

分析:

 首先考虑第一问:给定一个数k,求其顺序。数字之和比 k 小的数一定在 k 之前,我们 累加这些数。为此,我们需要一个函数 count(n,i),统计[1,n]中数字和为 i 的数有多少,实现 方法类似之前的例题,这里就不再赘述了。然后,我们需要求出在[1,n]中所有数字和为 sumof(k)的数满足字典序小于k 的数量。

 两个位数相同的数的字典序就是大小顺序,但由于数字不可以有前导 0,因此长度不同 的数字比较起来会很麻烦。为了避免这种情况,我们只比较位数相同的数:分别求出 k,k ×10,k×100,……以及k/10,k/100,……亦即,从1 到 n 枚举k 的位数并添 0 或删位。 对于每个长度的 k’,求出与之位数相同而小于k 的数的个数,也就等于 count(k’,sumof(k))-f[lengthof(k’)-1,sumof(k)]。这 里 ,记
号 sumof(k)表示k的数位之和,lengthof(k’) 表示 k’的十进制长度,f[i,j]是求count(n,i)时需要预处理的数据,表示所有长度为[0..10^i-1]的 数中各个数位之和为 j的数的个数。这样我们就求出了第一问。 

然后考虑第二问。依次将k减去count(n,1),count(n,2),…直到 k即将小于 0,则可确定答案的数字之和。接下来只需求出[1,n]中数字

之和为 s、字典序为k 的数是几。遗憾的是,因 为字典序不同于大小顺序,不可以利用第一问进行二分。我们可以使用一种较为暴力的逐位 确定方法,一位位确定答案的数位。由于答案位数未知,因此我们需要首先确定答案的首位 (枚举首位数字,用第一问的算法判断数量是否超过k)。然后,每次在已有的答案右边添 加一位,若添加之后合法数的个数恰为k 则返回当前答案,否则继续添加直到出解。由于答 案一定存在,添加过程不会超过 O(log(n))次。 

最终代码:

#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ULL;
ULL f[20][172];
ULL ten[20];

//f[i,j]表示所有长度为[0..10^i-1]的数中各个数位之和为j的数的个数
void init()
{
f[0][0]=1;
for (int i = 1;i < 20;i++ )
for (int j = 0;j <= i*9; j++ )
for (int k = 0;k <= 9; k++ )
if (j>=k)
f[i][j]+=f[i-1][j-k];
ten[0]=1;
for (int i=1;i<20;++i)
ten[i]=ten[i-1]*10;
}

//count(n,i)统计[1,n]中数字和为i的数的个数
ULL count(ULL n,int sum)
{
ULL ans=0;
for (int i=19;i>=1;--i)
{
sum-=n/ten[i];
if (sum<0) break;
n%=ten[i];
for (int j=0;j<n/ten[i-1];j++)
if (sum-j>=0)
ans+=f[i-1][sum-j];
}
if (sum==n)
ans++;
return ans;
}
int sumof(ULL n)
{
int sum=0;
while (n>0)
{
sum+=n%10;
n/=10;
}
return sum;
}
ULL find(ULL n,int sum,ULL k)
{
int i;
ULL k2=k,ans=0;
// 数字之和比 k 小的数
for (i = 1;i < sum;i++)
ans+=count(n,i);

for (i=0;ten[i+1]<=n;++i);

while(k<ten[i])
k*=10;

for (;k>0;k/=10,i--)
ans+=count(min(n,k-(k>k2)),sum)-f[i][sum];

return ans;
}
ULL calc(ULL n,ULL k)
{
int sum,i;
ULL ans,k2=k,tot;
for (sum=1;;sum++)
{
tot=count(n,sum);
if (k<=tot)
break;
k-=tot;
}

for (i=0;ten[i+1]<=n;i++);

for (ans=1;ans<=9;ans++)
{
tot=0;
for (int j=0;j<i;j++)
tot+=f[j][sum-ans];
if (ans<n/ten[i])
tot+=f[i][sum-ans];
else if (ans==n/ten[i])
tot+=count(n,sum)-count(ans*ten[i]-1,sum);
if (k<=tot)
break;
k-=tot;
}

while (find(n,sumof(ans),ans)!=k2)
{
ans*=10;
for (i=0;i<=9;i++)
if (find(n,sum,ans+i)>=k2)
break;
ans+=i;
if (find(n,sumof(ans),ans)==k2)
break;
ans--;
}
return ans;
}
int main()
{
// freopen("in.txt","r",stdin);
init();
ULL n,k;
while (scanf("%llu %llu",&n,&k)!=EOF&&(n+k))
{
printf("%llu %llu\n",find(n,sumof(k),k),calc(n,k));
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  动态规划 数位DP