动态规划10_数位DP2

文章来源:刘聪《浅谈数位类统计问题》


【例题 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));
	}
}


Python网络爬虫与推荐算法新闻推荐平台:网络爬虫:通过Python实现新浪新闻的爬取,可爬取新闻页面上的标题、文本、图片、视频链接(保留排版) 推荐算法:权重衰减+标签推荐+区域推荐+热点推荐.zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计,皆可应用在项目、毕业设计、课程设计、期末/期/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值