题目大意
问题描述
给定两个整数 n n n 和 v v v (1 ≤ v ≤ n ≤ 1 0 18 1 \le v \le n \le 10^{18} 1 ≤ v ≤ n ≤ 1 0 1 8 ),请计算在第 n n n 棵树 中,从根节点(节点 n n n )到节点 v v v 的最短路径上,所有经过节点的编号之和(包含起点 n n n 和终点 v v v )。
输入格式
一行包含两个整数 n n n 和 v v v 。
输出格式
输出一个整数 S S S ,表示路径节点编号之和。
样例数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Input 2 1 Output 3 Input 3 2 Output 5 Input 6 1 Output 12 Input 9 6 Output 23
样例解释
以 6 1 为例(输出 12):
T2 :根 2 2 2 ,边 ( 2 , 1 ) (2,1) ( 2 , 1 ) 。
T3 :T2 路径为 2 → 1 2 \to 1 2 → 1 。删除 ( 2 , 1 ) (2,1) ( 2 , 1 ) 。加入 3 3 3 ,连接 ( 3 , 2 ) , ( 3 , 1 ) (3,2), (3,1) ( 3 , 2 ) , ( 3 , 1 ) 。根 3 3 3 。
T4 :T3 根 3 3 3 ,子节点 { 2 , 1 } \{2, 1\} { 2 , 1 } ,最大子节点 2 2 2 (叶子)。路径 3 → 2 3 \to 2 3 → 2 。删除 ( 3 , 2 ) (3,2) ( 3 , 2 ) 。加入 4 4 4 ,连接 ( 4 , 3 ) , ( 4 , 2 ) (4,3), (4,2) ( 4 , 3 ) , ( 4 , 2 ) 。保留 ( 3 , 1 ) (3,1) ( 3 , 1 ) 。根 4 4 4 。
T5 :T4 根 4 4 4 ,子节点 { 3 , 2 } \{3, 2\} { 3 , 2 } ,最大子节点 3 3 3 (3 3 3 连 1 1 1 )。路径 4 → 3 → 1 4 \to 3 \to 1 4 → 3 → 1 。删除 ( 4 , 3 ) , ( 3 , 1 ) (4,3), (3,1) ( 4 , 3 ) , ( 3 , 1 ) 。加入 5 5 5 ,连接 ( 5 , 4 ) , ( 5 , 3 ) , ( 5 , 1 ) (5,4), (5,3), (5,1) ( 5 , 4 ) , ( 5 , 3 ) , ( 5 , 1 ) 。保留 ( 4 , 2 ) (4,2) ( 4 , 2 ) 。根 5 5 5 。
T6 :T5 根 5 5 5 ,子节点 { 4 , 3 , 1 } \{4, 3, 1\} { 4 , 3 , 1 } ,最大子节点 4 4 4 (4 4 4 连 2 2 2 )。路径 5 → 4 → 2 5 \to 4 \to 2 5 → 4 → 2 。删除 ( 5 , 4 ) , ( 4 , 2 ) (5,4), (4,2) ( 5 , 4 ) , ( 4 , 2 ) 。加入 6 6 6 ,连接 ( 6 , 5 ) , ( 6 , 4 ) , ( 6 , 2 ) (6,5), (6,4), (6,2) ( 6 , 5 ) , ( 6 , 4 ) , ( 6 , 2 ) 。保留 ( 5 , 3 ) , ( 5 , 1 ) (5,3), (5,1) ( 5 , 3 ) , ( 5 , 1 ) 。根 6 6 6 。
查询 :在 T6 中从 6 6 6 到 1 1 1 。由于 6 6 6 直接连接 5 5 5 ,且 5 5 5 此时保留了与 1 1 1 的连接(( 5 , 1 ) (5,1) ( 5 , 1 ) 在生成 T6 时未被选中删除),路径为 6 → 5 → 1 6 \to 5 \to 1 6 → 5 → 1 。
结果 :6 + 5 + 1 = 12 6 + 5 + 1 = 12 6 + 5 + 1 = 1 2 。
思路讲解
AI 提示 1
这道题的关键在于发现树的递归自相似结构。让我带你一步步找到规律。
观察一:根的孩子有什么规律?
把每棵树根节点的孩子列出来:
T 2 T_2 T 2 :孩子 = { 1 } \{1\} { 1 }
T 3 T_3 T 3 :孩子 = { 2 , 1 } \{2, 1\} { 2 , 1 }
T 4 T_4 T 4 :孩子 = { 3 , 2 } \{3, 2\} { 3 , 2 }
T 5 T_5 T 5 :孩子 = { 4 , 3 , 1 } \{4, 3, 1\} { 4 , 3 , 1 }
T 6 T_6 T 6 :孩子 = { 5 , 4 , 2 } \{5, 4, 2\} { 5 , 4 , 2 }
T 9 T_9 T 9 :孩子 = { 8 , 7 , 5 , 1 } \{8, 7, 5, 1\} { 8 , 7 , 5 , 1 }
规律:根 n n n 的孩子恰好是 n − 1 , n − 2 , n − 4 , n − 8 , … n-1, n-2, n-4, n-8, \ldots n − 1 , n − 2 , n − 4 , n − 8 , … (即 n − 2 j n - 2^j n − 2 j ,直到减出的值 ≤ 0 \le 0 ≤ 0 )。这可以通过分析最大子节点路径来证明——该路径恰好经过 n − 1 , n − 2 , n − 4 , … n-1, n-2, n-4, \ldots n − 1 , n − 2 , n − 4 , … 。
观察二:子树的自相似结构
第 j j j 个孩子 c j = n − 2 j c_j = n - 2^j c j = n − 2 j 的子树里,所有节点等间距排列,间距为 2 j + 1 2^{j+1} 2 j + 1 。例如在 T 9 T_9 T 9 中 :
孩子 8 8 8 (j = 0 j=0 j = 0 )的子树:{ 8 , 6 , 4 , 2 } \{8, 6, 4, 2\} { 8 , 6 , 4 , 2 } ,间距 2 2 2
孩子 7 7 7 (j = 1 j=1 j = 1 )的子树:{ 7 , 3 } \{7, 3\} { 7 , 3 } ,间距 4 4 4
孩子 5 5 5 (j = 2 j=2 j = 2 )的子树:{ 5 } \{5\} { 5 } ,间距 8 8 8
而且每个子树的内部结构 和某个更小的 T m T_m T m 完全同构!
观察三(核心):如何判断 v v v 在哪个子树里?
计算 d = n − v d = n - v d = n − v ,然后求 d d d 的二进制末尾零的个数 j = ν 2 ( d ) j = \nu_2(d) j = ν 2 ( d ) ,v v v 就在第 j j j 个孩子 n − 2 j n - 2^j n − 2 j 的子树中。
验证(T 9 T_9 T 9 中):
v = 6 v=6 v = 6 :d = 3 = 1 1 2 d=3=11_2 d = 3 = 1 1 2 ,j = 0 j=0 j = 0 → 在孩子 8 8 8 的子树 ✓
v = 3 v=3 v = 3 :d = 6 = 11 0 2 d=6=110_2 d = 6 = 1 1 0 2 ,j = 1 j=1 j = 1 → 在孩子 7 7 7 的子树 ✓
v = 1 v=1 v = 1 :d = 8 = 100 0 2 d=8=1000_2 d = 8 = 1 0 0 0 2 ,j = 3 j=3 j = 3 → 在孩子 1 1 1 的子树 ✓
观察四:递归下降求路径
进入子树后,"根"变为 c j c_j c j ,"步长"变为 b ⋅ 2 j + 1 b \cdot 2^{j+1} b ⋅ 2 j + 1 ,然后重复同样的过程。每一步步长至少翻倍,所以总共只需 O ( log n ) O(\log n) O ( log n ) 步。
整个算法伪代码:
1 2 3 4 5 6 7 8 9 sum = n, r = n, b = 1 while v ≠ r: d = (r - v) / b j = ν₂(d) // d 的末尾零个数 next = r - b × 2^j sum += next b ← b × 2^(j+1) r ← next 输出 sum
手动跑一下样例 9 6 :
r = 9 , b = 1 r=9, b=1 r = 9 , b = 1 :d = 3 , j = 0 d=3, j=0 d = 3 , j = 0 ,下一步 8 8 8 ,b ← 2 b \leftarrow 2 b ← 2 ,sum= 17 =17 = 1 7
r = 8 , b = 2 r=8, b=2 r = 8 , b = 2 :d = 1 , j = 0 d=1, j=0 d = 1 , j = 0 ,下一步 6 6 6 ,b ← 4 b \leftarrow 4 b ← 4 ,sum= 23 =23 = 2 3
r = 6 = v r=6=v r = 6 = v ,输出 23 23 2 3 ✓
时间复杂度 O ( log n ) O(\log n) O ( log n ) ,完全可以处理 1 0 18 10^{18} 1 0 1 8 。
AI 说的实在是,怎么说呢?讲的不是说很好啊,这 AI 讲的不是说很好,所以我认为真正的 AGI 啊,真正的 AGI 肯定是要会画图的。啊,现在的这个 AI 这个画图能力 比较一言难尽吧。
哎,不扯了,我们来看看这道题目。这道题目的规律,我这样子画了一张图啊,我觉得就很明显了。这就是所谓 AI 口中的自相似。你要说是自相似也也没什么问题,但是呢,自相似,但又没有那么相似。首先要注意到,根节点的孩子是 N 减一、 N 减二、 N 减四、 N 减八、 N 减十六 。云云。这个注意到还是有一定难度啊,还是有一定难度,因为他毕竟哎,毕竟他不是一个这个线性的规律,对吧?
然后你注意到这个规律以后呢,你可以把这个规律应用于下面的节点,它们的孩子节点是怎么样子的。我们会发现,如果说这个根节点是 N 减一,那么令 N 减一等于 N 那么它的叶子节点,它的孩子节点就是 N 撇减二、 N 撇减四、 N 撇减八。如果令 N 撇减二等于 N 撇撇。那么它的孩子节点就是 N 撇撇减四。
好,现在的问题就来到什么呢?你注意到了这个规律,要写一个程序,在 LogN 的时间复杂度内求解。
注意啊,就是减法非常难以考虑。你去减法的话,你就会发现,哎,他怎么退位呀,进位呀,反正反正很难考虑。但是不妨反过来想。我们是要判断一一个点,就这个数在哪棵子树内。那我们就想,它能不能通过某种方法去加到它的根,对吧?去加回到它的根。加法比减法好想的多 。
究其原因呢,加法比减法好想,其实是因为减法它会影响其后面的位,加法只会影响它前面的位。减法的这种回退的特性是我们最讨厌的。
判断的逻辑如此书写即可。不难发现的,我们实际上就是想要查看后 CI +1 位它的这个是否一样。之所以查看的是后 CI 加一位,是因为加法它只能改变 CI 加二位以及以后的位,它的这个情况。
1 2 3 4 5 6 7 8 9 10 11 auto check=[&](ll u,ll ci) -> bool { ll mask=(1ll <<(ci+1 ))-1 ; ll a=mask&V,b=mask&u; if (a==b) { return true ; } return false ; };
有了这个 check 函数以后,我们就写一个递归式的 solve 函数,就可以非常轻松的求解这个东西了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 auto solve=[&](auto && self,ll u,ll ci) -> void { ans+=u; if (u==V) { return ; } for (ll j=ci+1 ;;j++) { ll to=u-(1ll <<j); if (to<=0 ) { break ; } if (check (to,j)) { self (self,to,j); break ; } } };
AC代码
哎呀,这个 CF 挂的。今天是2月20号,这 CF 还是在,就是已经挂了5个多小时了。那么AI 我让 AI 对拍了一下这个程序是对的。啊,他拍了2000多组数据,都没发现这个 hack 那么这个应该就是对的。这个我这个也是一遍写出来的,挺爽的。
AC
https://codeforces.com/gym/106169/submission/363673850
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <bits/stdc++.h> #define all(vec) vec.begin(),vec.end() #define lson(o) (o<<1) #define rson(o) (o<<1|1) #define SZ(a) ((long long) a.size()) #define debug(var) cerr << #var <<" = [" <<var<<"]" <<"\n" ; #define debug1d(a) \ cerr << #a << " = [" ; \ for (int i = 0; i < (int)(a).size(); i++) \ cerr << (i ? ", " : "" ) << a[i]; \ cerr << "]\n" ; #define debug2d(a) \ cerr << #a << " = [\n" ; \ for (int i = 0; i < (int)(a).size(); i++) \ { \ cerr << " [" ; \ for (int j = 0; j < (int)(a[i]).size(); j++) \ cerr << (j ? ", " : "" ) << a[i][j]; \ cerr << "]\n" ; \ } \ cerr << "]\n" ; #define cend cerr<<"\n-----------\n" #define fsp(x) fixed<<setprecision(x) using namespace std;using ll = long long ;using ull = unsigned long long ;using DB = double ;using CD = complex<double >;static constexpr ll MAXN = (ll)1e6 +10 , INF = (1ll <<61 )-1 ;static constexpr ll mod = 998244353 ; static constexpr double eps = 1e-8 ;const double pi = acos (-1.0 );ll lT,testcase; void Solve () { ll N,V; cin >> N >> V; ll ans=0 ; auto check=[&](ll u,ll ci) -> bool { ll mask=(1ll <<(ci+1 ))-1 ; ll a=mask&V,b=mask&u; if (a==b) { return true ; } return false ; }; auto solve=[&](auto && self,ll u,ll ci) -> void { ans+=u; if (u==V) { return ; } for (ll j=ci+1 ;;j++) { ll to=u-(1ll <<j); if (to<=0 ) { break ; } if (check (to,j)) { self (self,to,j); break ; } } }; solve (solve,N,-1 ); cout<<ans<<"\n" ; } signed main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); cout.tie (nullptr ); #ifdef LOCAL cout.setf (ios::unitbuf); #endif Solve (); return 0 ; }