CF1870F - Lazy Numbers 一道Trie树思路应用的题目

C F 1870 F − L a z y   N u m b e r s \mathrm{CF1870F - Lazy\ Numbers} CF1870FLazy Numbers

D e s c r i p t i o n Description Description

对于给定的 n n n k k k,求解出 1 ∼ n 1\sim n 1n 的每一个数在 k k k 进制下字典序排列的顺序,输出满足数字本身为当前排好序后的下标的条件的数的个数( E X : 1 \mathrm{EX:} 1 EX:1 1 1 1 位置是满足条件的)。


S o l u t i o n Solution Solution

对于这种 k k k 进制的题目,可以想到用 Trie 树进行存储并快速计算,不过该题的 Trie 树的建树方式为由高位往低位建图,且不用补前导 0 0 0。(如图所示,此时为 n = 9 , k = 2 n=9,k=2 n=9,k=2 时的建图情况)
CF1870F - Lazy Numbers 一道Trie树思路应用的题目_第1张图片

由于要以字典序排列,故采用每一个点下面的边也按由小到大排列。这张 Trie 树的图就记录 1 ∼ n 1\sim n 1n 中的每一个数,其中字典序的顺序其实就是 DFS 的顺序,即一个点在按字典序排序之后的下标即为在 DFS 序中的下标。因为每一个点对应的边都是从小到大排列的,所以左侧的点一定字典序小,越往右字典序更大。

之后可以发现在同一层中,相邻的点一定相差 1 1 1,但是 DFS 序一定相差大于 1 1 1,故在第 i i i 层中,相差为 0 0 0 的一定是某一连续的段,且差值是满足单调性的,故可以二分。

那么接下来的问题就是如何求出 DFS 序:不可能进行一次 DFS 做一遍,因为 n n n 是达到 1 0 18 10^{18} 1018 级别的,必定 TLE。但是,可以发现求某个数的 DFS 序可以转化为求有多少个数在他之前访问。

对于转化后的问题,就好求多了,对于每一层,计算出位于这个数左边的数的个数:

CF1870F - Lazy Numbers 一道Trie树思路应用的题目_第2张图片

对于蓝色的数字个数,就是 6 6 6 在每一层时所对应的数的左边的数(包含该数),例如 6 6 6 在第 2 2 2 层所对应的数为 3 3 3,也就是有根节点到 6 6 6 号点所组成的链中的第 2 2 2 层的点。这个可以考虑 6 6 6 k k k 进制的前缀来计算,比如说 k = 2 k=2 k=2,那么二进制是 110 110 110:第一个前缀为 1 1 1,十进制为 1 1 1,故第二层所对应的数为 1 1 1;第二个前缀为 11 11 11,十进制为 3 3 3,故第二层所对应的数为 3 3 3;第三个前缀为 110 110 110,十进制为 6 6 6,故第三层对应的数为 6 6 6

由于在 k k k 进制下第 i i i 层最左侧的数为 k i k^i ki,其中 i i i 0 0 0 开始,即 1 1 1 的那一层为第 0 0 0 层。那么这一层比他小的数为:前缀 i − k i + 1 i - k^i+1 iki+1。比如说,第 2 2 2 层( 6 6 6 的那一层),就应该是 6 − 2 2 + 1 = 3 6-2^2+1=3 622+1=3

这样,就可以快速的求解出蓝色部分的值了,但是还有绿色部分也比 6 6 6 的 DFS 序小,所以如果未到这棵树的最后 1 1 1 层,可以通过尾部不断加入 0 0 0 来继续计算(计算方式与上文一致),不过在最后一层要特别注意,可能是不满的,所以此时不能再用前缀减 k i k^i ki 了,而是用 n n n k i k^i ki 1 1 1。例如, 6 6 6 在末尾加 0 0 0 直到达到整棵树的深度: 1100 1100 1100,这时候在第 3 3 3 层时,不能直接用当前数减 8 8 8 1 1 1,而是用 9 9 9,因为 9 9 9 12 12 12 1100 1100 1100)之间的数是没有的。注意:这里 12 12 12 不能算入比 6 6 6 小的因为这是方便计算加入的 0 0 0 才出现的数,不能计入贡献。

最后拿 3 3 3 给大家展示一下全过程:

CF1870F - Lazy Numbers 一道Trie树思路应用的题目_第3张图片
  1. 蓝色部分:

    (1)第 0 0 0 层: 1 1 1(二进制 1 1 1 − 2 0 + 1 = 1 - 2^0+1=1 20+1=1

    (2)第 1 1 1 层: 3 3 3(二进制 11 11 11 − 2 1 + 1 = 2 -2^1+1=2 21+1=2

  2. 绿色部分:将 3 3 3 补完 0 0 0 后得到 1100 1100 1100,数字为 12 12 12

    (1)第 2 2 2 层: 6 6 6(二进制 110 110 110 − 2 2 + 1 − 1 = 2 -2^2+1-1=2 22+11=2(注意不能包含 6 6 6

    (2)第 3 3 3 层: 9 − 2 3 + 1 = 2 9-2^3+1=2 923+1=2(最后 1 1 1 层用 n n n 减)

最后将贡献相加: 1 + 2 + 2 + 2 = 7 1+2+2+2=7 1+2+2+2=7 3 3 3 的 DFS 序为 7 7 7 位置。

至此,该题目就可以在 O ( log ⁡ 2 n log ⁡ k 2 n ) O(\log_2n\log_k^2n) O(log2nlogk2n) 的时间复杂度通过该题目了,注意某些位置要开 __int128,不过尽量减少使用,详见代码。


C o d e Code Code

#include 
#define int long long

using namespace std;

typedef __int128 i128;

int N, K, L, Pow_K[100], KX[100], S1, S2;

int DFS(int X)
{
	int Result = 0; i128 DEC = 0; S1 = 0, S2 = 0;
	while (X) KX[S1 ++] = X % K, X /= K;
	reverse(KX, KX + S1);
	for (int i = 0; i < S1; i ++) DEC = DEC * K + KX[i], Result += DEC - Pow_K[i] + 1;
	for (int i = S1; i < L; i ++) DEC = DEC * K, Result += min(DEC - 1, (i128)N) - Pow_K[i] + 1;
	return Result;
}

void solve()
{
	cin >> N >> K;

	i128 T = 1; int M = 0, Cpy = N; L = 0, Pow_K[0] = 1;
	while (T * K <= (i128)N) T *= K, M ++, Pow_K[M] = T;
	while (Cpy) Cpy /= K, L ++;

	int Result = 0;
	for (int i = 0; i < L; i ++)
	{
		int l = Pow_K[i], r = Pow_K[i + 1] - 1, l2 = Pow_K[i], r2 = min(Pow_K[i + 1] - 1, N);
		if (i == L - 1) r = N, r2 = N;
		while (l < r) DFS(l + r >> 1) - (l + r >> 1) < 0 ? l = (l + r >> 1) + 1 : r = (l + r >> 1);
		while (l2 < r2) DFS((l2 + r2 + 1 >> 1)) - (l2 + r2 + 1 >> 1) > 0 ? r2 = (l2 + r2 + 1 >> 1) - 1 : l2 = (l2 + r2 + 1 >> 1);
		if (DFS(l) != l) continue;
		Result += r2 - l + 1;
	}

	cout << Result << '\n';
}

signed main()
{
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(0);

	int Data;

	cin >> Data;

	while (Data --)
		solve();

	return 0;
}

F i n a l    W o r d s Final\ \ Words Final  Words

  1. 题目细节很多,还要注意减少 __int128 的使用,写代码的时候要耐心写
  2. 这道题目值得大家取仔细思考,比如说一开始我写了一个 O ( log ⁡ 2 n log ⁡ k 3 n ) O(\log_2n\log_k^3n) O(log2nlogk3n) 的复杂度,之后 TLE 了,才改成 log ⁡ k 2 n \log^2_k n logk2n 的。
CF1870F - Lazy Numbers 一道Trie树思路应用的题目_第4张图片

​ 一张坎坷 A C \bold{\color{green}{AC}} AC 历程给大家展示

  1. 这道题目主要考察了 Trie 树,却没有真正的需要写 Trie 树,而是通过二分与数学推导的结合来实现这到题目,可谓是及其的妙!
  2. 也是本蒟蒻做过不多的紫题中的一道,研究了 2 2 2 天,在此纪念一下~~~

你可能感兴趣的:(Codeforces,算法,Codeforces,Trie)