[超详细题解]Cute Sequences (CodeForces

题意:q个询问,每个询问给出1到1e14之间的整数a、b和m,问a经过一系列步骤之后能否变换到b,如果能输出变换过程,如果不能输出-1。这里的一系列变换指:当前数等于之前所有数之和加上r,其中r的范围是1 <= r <= m,也就是说,r的下限是1,每次经过一个步骤之后下一个数最小也是前面所有数的和加1,题目给出变换过程中所有数的个数是k个,其中1 <= k <= 50,也就是说,变换的这一系列序列最多含有50个数。

分析:考虑不加r的情况,第一个数是a,第二个数等于它之前所有数的和,它之前只有a,所以第二个数也是a,第三个数等于第一个数加第二个数等于(1+1) * a,第四个数等于前三个数的和等于 (1 + 1 + 2) * a,以防万一再写一项,第五个数等于(1 + 1 + 2 + 4) * a,发现了吗,这个序列是1,1,2,4,8……,可想而知,考虑第k个数,如果k < 2,那么a的个数是1,如果k >= 2,那么a的个数是2^(k-2)个。有了这个思路,我们考虑r,第一个数是a,第二个数实际上我们要等于a+r(其中1<=r<=m),因此第二个数的取值范围是a+1到a+m的,那么同理第三个数的范围应该是2a + 2 到2 * a + 2 * m的,第四个数是范围是4a + 4到4 * a + 4 * m的,以此类推,那么我们可以知道,当k>=2时,第k个数的范围是2^(k-2) * (a+1) 到 2^(k-2) * (a+m),由于题目已经告诉我们k最大也是50个,所以我们可以枚举k,当我们发现b在[2^(k-2) * (a+1) , 2^(k-2) * (a+m)]这个区间中时,我们记录下当前的k的值,如果遍历完k也找不到满足在这个区间中的b,那么直接输出-1就好,值得注意的是,当左端点 >b时便不需要继续遍历了,因为后面的区间肯定不满足条件,继续遍历可能会因为爆long long而导致答案出错。下面是这一段的代码:

/*#define int long long & typedef long long ll & signed main()*/
int pos = -1;
for (register int k = 2; k <= 50; k++) {
	int pow = (1ll << (k-2)); //注意是1ll 本题数据过大必须是64位的
	if ((a+1)*pow > b) break;
	if ((a+1) * pow <= b && b <= (a+m) * pow){
		pos = k;
		break;
	}
}
if (pos == -1) {
	cout << "-1" << endl;
	continue;
}

构造答案:此题最麻烦的地方就在于如何把变换过程输出,在上一步中我们找到了满足条件的b,这个b在这个区间里:[2^(k-2) * (a+1) , 2^(k-2) * (a+m)],并且我们记录下了满足条件下的这个k的值,这里记为pos。我们需要知道每一个元素要加的r是多少,首先因为1<=r<=m,每个元素最少都要加上1,因此我们先把b - 2^(k-2) * (a+1)的值记为remind,表示剩下还需要加多少数才能到b,然后我们考虑某一位加上1之后对最后b的贡献,这样说可能有点抽象,举个例子,给一个数列1,1,2,4,8。这个数列是最原始的数列,也就是每个数都等于它前面所有数的和,现在我把第二个1的r变为1,其余r都假定先为0,也就是说,这个序列变成了1,2,3,6,12,我们看最后一位,由原来的8变成了12,也就是说加了4,因此把从右往左的第4个位置+1,对最后一个元素的贡献是4,为什么是从右往左?因为当前位置的值增加后只会对后面的数有贡献,也就是后面的数的值是由前面元素的和计算出来的。现在继续举几个例子,把从右往左的第三个数,也就是2处的r变为1,其他的r假定为0,这个序列变成了1,1,3,5,10,最后的元素变成了10,也就是说从右往左第3个位置+1,对最后一个元素的贡献是2,同理,从右往左第2个位置+1,对最后一个元素的贡献是1,这里可以发现,从右往左的第k个数字+1,对最后元素的贡献应该是2^(k-2)。回过头来看之前的remind,我们把b - 2^(k-2) * (a+1)的值记为remind,什么意思呢,表示从左端点开始还要贡献多少才能到b,需要贡献的数量就是remind。通过之前的推导我们知道了对某个位置+1实际上对最后元素的贡献是一个二进制数(2的某某次方),所以对某个位置+m当然也对最后元素的贡献也是这个二进制数的m倍,我们知道任何数都能用二进制数表示,当然remind也可以,更何况我们用的不仅仅是二进制数还是1~m倍的二进制数(一个二进制数用1到m次都行),注意这里我们其实并不是1到m,而是0到m-1,因为之前已经把一个1加到左端点里去了(因为r的下限是1,所以不妨直接先把它加进去),所以我们实际上每一位可以选用做贡献的二进制数的个数是0到m-1个(代码里的m--的含义)。下面讲解构造方法,这里采取一种贪心的策略,我们从最大的二进制数开始选择,这个二进制数可以选0到m-1次,我们选取最多能选取的次数(如果能选就尽量选,什么叫能选?就是不超出remind的范围和选取的个数在0到m-1次的范围内)当我们对最高位选择结束后,remind的值应该减小最高位对最后一个元素的贡献,然后继续枚举最高位的后面一位,重复此操作,直到remind的值变为0,代表此时此刻已经不需要再增加r的值对最后一个元素做贡献了,此时计算到最后最后的元素就是b。对这个思路的实现,我们用一个nb数组记录当前位需要做贡献的次数,然后输出的话只要按题目的定义,计算前缀和然后加上r就能算出整个序列的元素。当a=b时特判一下直接输出a或者b就好。具体实现方式请看代码及注释。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#define int long long
typedef long long ll;
using namespace std;
int q, a, b, m, k;
int nb[100];//记录某个坐标下r-1的值(贡献次数)
int sum[100];//前缀和数组
signed main(){
	cin >> q;
	while (q--) {
		memset(nb,0,sizeof nb);
		cin >> a >> b >> m;
		if (a == b) {
			cout << "1 " << a << endl;
			continue;
		}
		int pos = -1;
		for (register long long k = 2; k <= 50; k++) {
			long long pow = (1ll << (k-2));
			if ((a+1)*pow > b) break;
			if ((a+1) * pow <= b && b <= (a+m) * pow){
				pos = k;
				break;
			}
		}
		if (pos == -1) {
			cout << "-1" << endl;
			continue;
		}
		cout << pos << " " << a;//从a开始
		int remind = b - (a+1) * (1ll << (pos-2));//b的值-左端点的值=所需的贡献
		m--;//已经对每个元素+1,所以还能做的贡献为0到m-1
		for (register int i = pos,cnt = 2; remind; i--, cnt++) {//最高位开始枚举
			int val;
			if (i-3<0) val = 1;
			else val = 1ll << (i-3);//实际上就是从右往左某某位的贡献 模拟一下便可推出
			int num = remind / val;//remind最多能由几个最高位组成 该值需要判断一下是否超出m-1
			if (num <= m) {//这里的m是已经经过了m--的m  相当于最早的m-1
				remind -= num * val;
				nb[cnt] = num;
			} else {
				remind -= m * val;
				nb[cnt] = m;
			}
		}
		sum[1] = a;
		for (register int i = 2; i <= pos; i++) {
			int tmp = 0;
			for (register int j = 1; j < i; j++) tmp += sum[j];//求前缀和
			sum[i] = tmp + 1 + nb[i];//+1代表下限的1,nb[i]代表还需要做贡献的次数
			cout << " " << sum[i];
		}
		cout << endl;
	}
	return 0;
}
原文地址:https://www.cnblogs.com/hznudreamer/p/12776471.html