当天比赛打完之后看了看跟我排名差不多的人的这题代码,感觉莫名其妙,写了几十行。我看了看我只有十几行的 AC 代码,陷入了沉思。
分析
题目的要求其实可以转换为:在区间 \([l_0, r]\) 中选择一些数,使得这些数排序后每个数都是前一个数的倍数,要求选的尽可能多。那既然要选的尽可能多,左端点肯定要选上,并且应该让每个数都是前一个数的两倍。这样才能使得选出的最多。设最多选出 \(k + 1\) 个数,那么有式子 \(l_0 \times 2^k \le r\) 成立,即选出的最大数小于等于区间右端点。于是得出 $k \le \log_2(\frac{r}{l_0}) $。于是 \(k\) 的最大值就是 $ \lfloor \log_2(\frac{r}{l_0}) \rfloor$。令 \(k\) 取最大值,则输出的第一个答案就是 \(k + 1\)。
接下来题目让我们求这样的集合个数。显而易见的一个暴力就是从左端点开始枚举,一直枚举到不存在符合要求的集合。这样可能会炸,所以考虑优化。
考虑二分答案。
怎么可能二分答案(虽然也不是不行)!显然我们可以把刚才那个式子拿过来用:\(l \times 2^k \le r\)。移项得 \(l_1 \le \frac{r}{2^k}\)。令 \(l_1\) 取最大值 \(\lfloor \frac{r}{2^k} \rfloor\),则在每个数都是前一个数的两倍的情况下,左端点 \(l \in [l_0, l_1]\)。所以答案应该是 \(l_1 - l_0 + 1\),因为从这个范围内的左端点开始往右都可以构造一个满足要求的集合。
你真以为就这么简单?
不可能的。
显然,在使选出的数最多的情况下,后一个数并不一定非要是前一个数的两倍。也可以是,比如说三倍,四倍什么的。在抱怨这题之前,让我们先仔细看看:后面一个数可以是前一个数的四倍吗?
显然不行。
若后面一个数是前一个数的四倍,那完全可以在中间再插一个数进去,使选出的数变多。顺着这个思路往下想,后面的数是前面数的五倍,六倍,甚至七倍,八倍,那显然也不行。所以在使得选出的数最多的情况下,后面的数最多是前面数的三倍。
猜你在想:排列组合。
你先别急。让我们接着想:若在选出的集合中有三个连续的数,分别叫 \(x_1, 3x_1, 9x_1\)。这个时候,我们发现,这三个数可以被四个数代替:\(x_1, 2x_1, 4x_1, 8x_1\)。这样,选出的数就又变多了。
发现什么问题?刚才的讨论说明了后面的数最多是前面数的三倍,而且最多只能有一个数是其前面数的三倍。于是我们可以再算出
一个临界点 \(l_2\),使得从 \([l_0, l_2]\) 中任意一个左端点开始都可以构造 \(k\) 个符合要求的集合(除此集合的左端点外,共有 \(k\) 个数可以是其前面数的三倍),而从 \((l_2, l_1]\) 中任意一个左端点开始都只能构造一个符合要求的集合。那接下来的任务就是算出 \(l_2\)。
什么?你不会还想二分答案吧!别啊,我们有更加迅速的做法:先前那个式子 \(l \times 2^k \le r\) 在这边改一改还能用:\(l_2 \times 3 \times 2^{k-1} \le r\),也就是把其中一个 2 换成 3。移项得 \(l_2 \le \frac{r}{3 \times 2^{k - 1}}\)。令 \(l_2\) 取最大值 \(\lfloor \frac{r}{3 \times 2 {k - 1}} \rfloor\),则答案为 \((l_2 - l_0 + 1) \times k + (l_1 - l_0 + 1)\)。
你真以为就这么简单?
是的,真就这么简单。写的时候注意一下精度问题就好了。
这里最终的 \(l_2\) 算出来可能会小于 \(l_0\),需要注意。
代码
#include <iostream>
#include <math.h>
#define int long long
using namespace std;
const int p = 998244353;
signed main() {
int tc;
cin >> tc;
while (tc--) {
int a, b;
cin >> a >> b;
int x = log2((1.0 * b) / a);
int l = a, m = (b / 3) / (1 << x - 1), r = b / (1 << x); // l 是 l0, r 是 l1,m 是 l2,x 是 k。
cout << x + 1 << " " << (r - l + 1 + (((m - l + 1) % p) * (x % p) * (m >= l)) % p) % p << "\n";
}
return 0;
}