我发一个代码 帮我优化时间复杂度#include<iostream> #include<algorithm> #include<queue> #define int long long using namespace std; int n, m, k,t; queue<int>qu; //先写一个找x,y的函数 //然后入队,推出孩子节点坐标,根据孩子节点坐标转化为第几个节点,判断这个节点是否在范围内 int child(int k,int m)//计算孩子节点,第一个孩子节点就是本层前面节点数量x完全数 { return k * m + 2 - m; } void solve(int n, int m, int k)//n个节点,完全m树,求k下面节点的数量,i是记录答案(ans[i]++) { int ans=0; if (k < 1 || k > n) { ans = 0; return; } qu.push(k); while (!qu.empty())//队列非空 { for (int j = 0; j < m; j++)//所有的孩子节点 { int a = child(qu.front(), m); a += j;//找另外几个孩子节点 if (a <= n)//判断孩子节点是否在树里面 qu.push(a); } ans++;//记录数量 qu.pop();//出队 } cout << ans << endl; } signed main() { cin >> t; for (int i = 1; i <= t; i++) { cin >> n >> m >> k; solve(n, m, k); } return 0; }
时间: 2025-03-22 14:11:38 浏览: 61
<think>嗯,我现在要优化这个代码的时间复杂度。首先,我需要理解原代码的功能和结构。原代码是处理一个完全m叉树,给定n个节点,求某个节点k的子节点数目。看起来用的是广度优先搜索(BFS)的方式,每次将当前节点的所有子节点入队,然后统计总的节点数目。这样时间复杂度可能比较高,尤其是当树很大时,BFS需要遍历很多节点,导致时间不够。
那原代码的时间复杂度是多少呢?假设每次处理一个节点,每个节点有m个子节点,那么最坏情况下,如果整个树都是k的子树,那么节点数可能是O(m^d),其中d是树的深度。这样时间复杂度是O(m^d),这显然对于大的m或者d来说,会很慢。比如,当m=1000,d=10的话,就是1000^10次操作,这显然不可行。所以需要找到更优的算法,将时间复杂度降低到O(1)或者O(log n)之类的。
那问题转化为,如何在不遍历的情况下计算以k为根的子树的节点数。这可能涉及到完全m叉树的结构分析。
首先,我需要回忆完全m叉树的结构。在完全m叉树中,每个节点都有m个子节点,除了最后一层的节点可能不满。而题目中的n个节点构成的树可能不是满的,所以需要考虑最后一层的情况。
假设节点编号是按照层次遍历的顺序来排列的。比如根节点是1,它的子节点是2,3,...,m+1;然后每个节点的子节点依次排列。例如,节点k的子节点是k*m + 2 - m到k*m +1。例如,当m=3时,节点1的子节点是2,3,4;节点2的子节点是5,6,7;节点k的子节点起始于k*3 -1 +1?或者原代码中的child函数是计算第一个子节点的位置?
原代码中的child函数是:k*m + 2 - m。比如,当m=3时,k=1的话,child是1*3 +2 -3=3+2-3=2。所以第一个子节点是2,然后依次加j。那每个节点k的第j个子节点是child(k,m)+j,其中j从0到m-1?例如k=1,m=3,第一个子节点是2,然后j=0得到2,j=1得到3,j=2得到4。是的,这样是正确的。所以每个节点的子节点范围是[k*m +2 -m +0, k*m +2 -m + (m-1)],即k*m +2 -m到k*m +2 -m + m-1 =k*m +1。所以每个节点k的子节点的范围是k*m +2 -m到k*m +1。比如k=1,m=3的话,2到4。对的。
那么,现在的问题是,给定节点k,求以k为根的子树中的节点数目。对于完全m叉树来说,如果该子树是一棵满的m叉树,那么节点数目是 (m^h -1)/(m-1),其中h是树的高度。但实际情况中,子树可能不满,尤其是在整个树的最后一层。
所以,需要找到两个参数:该子树的层数h,以及最后一层的节点数目。然后总的节点数目就是满的层数的节点数加上最后一层的数目。
那么如何计算这个呢?
首先,我们需要确定以k为根的子树的最小可能的层数和最大可能的层数。例如,该子树的最底层是否超过了整个树的最底层。比如,整个树共有n个节点,而k的子树可能无法完全展开到最大深度。
这里需要找到以k为根的子树的最大可能深度,以及最后一层的节点数目。或者,计算该子树中的节点的数目,可以转化为数学问题,找到该子树的最底层是否在整体树的节点范围内。
例如,假设整个树的最底层的起始节点是某个值,而子树的最底层可能在更早的位置。
现在,我需要找出该子树的起始节点k所在的层,以及整个树的层数结构。例如,整个树的高度可以通过计算,然后子树的高度由k的位置决定。
或者,可以这样考虑:以k为根的子树的节点数目等于,从k出发,逐层计算每层的节点数目,直到无法再填满为止。
例如,子树的每个层级的节点数目是m^0, m^1, m^2,...,直到某一层无法填满。总共有h层,其中h是最大的层数使得k的子树的最后一个节点不超过n。
那么,如何计算这个h和对应的总节点数?
首先,子树中的节点都是k的后代,按层次遍历的顺序排列。比如,k是根,它的子节点是k的子节点,然后是子节点的子节点,等等。这些节点的编号是连续的,并且可以按照完全m叉树的结构来排列。
假设整个树是完全m叉树,那么子树k的节点数目可以用数学方法计算,而不需要遍历。
那么,我们需要确定子树k的最大的层数,使得该层的最后一个节点不超过n。然后总节点数是各层的总和加上最后一层的实际节点数。
例如,子树的第0层是k本身,数量为1。第1层有m个节点,第2层有m^2个,直到第d-1层。最后一层可能有不足m^{d-1}个节点。
所以,我们需要找到最大的d,使得:
sum_{i=0}^{d-1} m^i = (m^d -1)/(m-1) 的总节点数对应的起始节点k的位置和该子树的最大编号不超过n。
或者,更准确地说,子树中的节点编号范围是:k的最左后代在第d层的编号是k*(m)^d - (m^{d} -1)/(m-1) +1?或者是否有其他方式计算?
或者,另一种思路是,以k为根的子树的所有节点的编号形成一个连续的区间吗?这可能不一定正确,因为完全m叉树的结构可能并不是这样。例如,父节点的子节点的编号可能不是连续的,而是分散的。比如,当m=2时,父节点k的子节点是2k和2k+1。但原问题中的子节点编号方式可能与常规的二叉树不同。例如,原问题中的子节点计算方式可能不同。
例如,原问题中的child函数给出的是k的第一个子节点的编号是k*m +2 -m。对于不同的k和m,这个计算是否正确?
比如,当m=2时,k=1的第一个子节点是1*2 +2 -2=2,第二个子节点是3。对于k=2,第一个子节点是2*2+2-2=4,子节点是4和5。这看起来是对的。所以,这样的编号方式下,每个节点的子节点的编号是连续的,即第一个子节点是k*m +2 -m,第二个是k*m +3 -m,直到k*m +1。例如,当m=3时,k=1的子节点是2,3,4;k=2的子节点是5,6,7;k=3的子节点是8,9,10。所以,编号是按层次遍历的顺序排列的,每个节点的子节点是连续的。
所以,整个树的结构是按照层次遍历的顺序编号的。每个节点的子节点的编号是连续的,并且第一个子节点的编号为k*m +2 -m,最后一个子节点的编号为k*m +1。比如,m=3时,k=1的子节点是2,3,4(对应到式子k*m+2 -m +j,其中j从0到2)。
现在,子树k的所有节点的编号是连续的?或者可能不是?例如,当m=2,k=2的子节点是5和6。假设n足够大,那么k=2的子树包括5、6、他们的子节点等等。这些子节点的编号可能不是连续的,比如5的子节点是5*2+2-2=10和11,6的子节点是12和13。那么k=2的子树包括5、6、10、11、12、13等,这些编号并不是连续的,所以不能用连续区间的方式处理。
因此,不能简单地将子树视为连续的区间,所以必须找到另一种数学方法计算该子树的节点数目。
这时,可以考虑树的结构,每个节点的子节点数量为m,那么子树的结构是完全m叉树。因此,子树中的节点数目等于该完全m叉树的节点数目,除非树的最后一层无法填满。
因此,我们需要计算,以k为根的完全m叉树中,最多能有多少个节点,不超过总节点数n。
这可以分为两种情况:
1. 以k为根的子树是一棵满的m叉树,那么总节点数为 (m^h -1)/(m-1),其中h是树的高度。
2. 子树的最后一层没有填满,此时总节点数为满层的节点数加上最后一层的节点数。
但如何计算h?
首先,假设k所在的层是某个层,比如原树中的层数不影响子树的结构,因为子树的结构独立于原树的结构。也就是说,不管原树如何,以k为根的子树本身的结构是一个完全m叉树,但可能被原树的节点数n所限制。
此时,我们需要找到最大的h,使得子树中的节点数目不超过n。或者说,子树中的最大节点编号不超过n。
例如,以k为根的子树的每一层的节点数目为1, m, m^2, ..., m^{h-1}。总共有h层。这些节点的编号如何确定?
或者,更直接地说,子树中的每个节点的编号可以通过某种方式计算出来。比如,根k的层是0,其子节点层是1,每个节点的子节点的编号可以通过child函数计算。
但这里的问题是,如何找到子树中所有节点的数目,而不需要遍历每个节点。这可能涉及数学上的递推或几何级数。
另一个思路是,子树中的每一层的第一个节点和最后一个节点的编号可以被计算出来。如果最后一层的最后一个节点的编号<=n,则这一层是满的;否则,只有部分节点存在。
例如,根节点是k,层0有1个节点,即k。层1有m个节点,从c_0到c_0 +m-1,其中c_0是child(k,m)。层2有m^2个节点,每个节点的child被展开。每一层的节点数目是m^i。
所以,子树中的每一层的起始节点和终止节点可以计算出来。例如,层i的起始节点是k*m^i + (m^{i} -1)/(m-1) + 1 -m^i ? 这里可能需要找到一种数学表达式。
或者,可以认为,以k为根的子树的层i的节点数目是m^i,这些节点的编号范围的最小值是l_i,最大值是r_i。那么,对于每一层i,如果r_i <=n,则这一层的所有节点都存在于原树中;否则,只有部分存在。
总节点数目等于前h层的满层数目之和,加上第h+1层的部分数目。
所以,现在的问题转化为,找到最大的层数h,使得层h的最后一个节点r_h <=n。此时,总节点数目是sum_{i=0}^h m^i} + max(0, (n - l_{h+1} +1 )) ),如果层h+1存在部分节点的话?
或者,可能我需要重新推导每个层的起始和结束编号。
例如,根k在第0层,数目1,起始和结束都是k。
层1的节点数目是m,起始是c = child(k, m) =k*m +2 -m,结束是c +m -1 =k*m +1.
层2的节点数目是m^2,起始是child(c, m) =c*m +2 -m。每个节点在层1的每个节点生成m个子节点。所以层2的起始是c*m +2 -m,结束是c*m +2 -m +m^2 -1 =c*m +1 +m^2 -m.
或者,或许有更简洁的表达式。例如,对于层i来说,起始编号是k * m^i + (m^{i} -1)/(m-1) +1 -m^i。或者,可能我的数学推导还不够到位。
或者,可以观察规律。例如,当m=2,k=1时:
层0:1 → 编号1
层1:2,3 → 起始2=1*2+2-2=2,结束3=2+1=1*2+1=3.
层2:每个层1的节点生成两个子节点,所以层2的节点是4,5,6,7 → 起始4=2*2+2-2=4,结束7=4+3=7.
层i的起始编号是k*m^i + (m^{i} -1)/(m-1) - (m^{i} -1) ?
或者,可能这个思路比较复杂。有没有更好的方法?
另一种思路是,以k为根的子树中的每个节点的编号遵循一定的生成规律。例如,每个节点的子节点的范围是k*m +2 -m到k*m +1。因此,子树中的节点可以视为按照层次展开,每层的节点数目是m^i(i是层数,从0开始)。那么,每层的起始编号可以通过递推得到。
例如,层0:起始k,数目1.
层1:起始k*m +2 -m =k*m -m +2.
数目m.
层2:起始是层1每个节点的第一个子节点的最小值,即层1起始节点的第一个子节点,即 (k*m -m +2)*m -m +2 =k*m^2 -m^2 +2m -m +2 =k*m^2 -m^2 +m +2.
数目m^2.
以此类推,层i的起始编号为:
start_i =k*m^i - (m^{i} -m)/(m-1) +1 ?
或者,是否有这样的递推公式?
或者,可以观察层i的起始编号是:
层0:start_0 =k
层1:start_1 =k*m +2 -m
层2:start_2 =start_1 *m +2 -m = (k*m +2 -m)*m +2 -m =k*m^2 +2m -m^2 +2 -m =k*m^2 -m^2 +m +2.
层3:start_3 =start_2 *m +2 -m = [k*m^2 -m^2 +m +2] *m +2 -m =k*m^3 -m^3 +m^2 +2m +2 -m =k*m^3 -m^3 +m^2 +m +2.
看来这样的规律比较难以用简单的数学表达式表示。可能需要找到另一个方法。
或者,可以注意到,层i的起始编号等于k乘以 m^i,然后加上一个修正项。例如,对于层i来说,起始编号可能是:
start_i =k * m^i + (m^{i} -1)/(m-1) - (m^{i} -1) ?
或者,可能我的思路有问题。或许应该寻找每个层i的最小和最大编号,并找到这些编号与n的关系。
假设层i的最小编号为 l_i,最大编号为 r_i。那么:
层0:l_0 =k, r_0 =k.
层1:l_1 =k*m +2 -m, r_1 =k*m +1.
层2:l_2 =l_1 *m +2 -m = (k*m +2 -m)*m +2 -m =k*m² +2m -m² +2 -m.
r_2 =r_1 *m +1 = (k*m +1)*m +1 =k*m² +m +1.
层3:l_3 =l_2 *m +2 -m.
r_3 =r_2 *m +1.
以此类推。那么,对于每个层i,我们可以计算l_i和r_i。当l_i超过n时,这一层及之后的层都不存在。否则,如果r_i <=n,则这一层全部存在;否则,这一层部分存在,最多到n。
因此,总节点数目等于所有满层的数目之和,加上最后一层的部分数目。
那么,问题转化为如何高效计算每个层的l_i和r_i,直到找到最大的i,使得l_i <=n。然后统计这些层的节点数目之和,加上可能的最后一层的部分数目。
这显然可以避免使用BFS,而改用数学方法逐层计算,直到超过n为止。这样时间复杂度将取决于层数,而不是节点数目。例如,层数最多是log_m(n)级别,所以时间复杂度为O(log n),这比原来的O(m^d)要好得多。
现在,如何具体实现这个思路?
步骤大概是这样的:
初始化当前层数为0,当前层的起始和结束都是k。总节点数ans=0。循环:
1. 计算当前层的起始l和结束r。
2. 如果l >n,说明这一层及之后的层都不存在,退出循环。
3. 否则,如果当前层的结束r <=n,则这一层的节点数目是m^current_layer,加到ans上。
4. 否则,这一层只有部分节点存在,数目是n -l +1,加到ans上,并退出循环。
5. 否则,继续到下一层,计算下一层的l和r。
然后,最后ans就是所求的答案。
现在,如何计算每一层的l和r?
观察层i的l和r的递推关系:
层0:
l0 =k
r0 =k
层1:
l1 =k*m +2 -m
r1 =k*m +1
层2:
l2 =l1*m +2 -m
r2 =r1*m +1
...
层i的l_i和r_i:
l_i = l_{i-1} *m +2 -m
r_i = r_{i-1} *m +1
或者,有没有更简洁的表达式?
比如,对于层i来说:
层i的起始l_i =k * m^i + (2 -m) * (m^{i} -1)/(m-1)
这可能吗?
例如,对于层1:
i=1,l1 =k*m + (2 -m)* (m^1-1)/(m-1) =k*m + (2 -m)*( (m-1)/(m-1 )) )=k*m + (2-m)*1= k*m +2 -m,符合。
对于层2:
l2 =k*m^2 + (2 -m)*( (m^2-1)/(m-1 )) )=k m² + (2 -m)(m+1) =k m² + (2m +2 -m² -m) =k m² -m² +m +2.
这与之前的递推结果一致:
层2的l2 =l1*m +2 -m = (k*m +2 -m)*m +2 -m =k*m² +2m -m² +2 -m =k m² -m² +m +2.
所以,这个公式是正确的。所以,通用公式为:
l_i =k *m^i + (2 -m) * (m^i -1)/(m-1 )
同样,对于r_i:
层0的r0 =k.
层1的r1 =k*m +1.
层2的r2 =r1*m +1 = (k*m +1)*m +1 =k*m² +m +1.
层i的r_i =k *m^i + (m^i -1)/(m-1 )
例如,i=1:
r1 =k*m + (m^1 -1)/(m-1) =k m + (m-1)/(m-1)=k m +1 → 正确。
i=2:
r2 =k m^2 + (m^2 -1)/(m-1) =k m² + m+1 → 正确。
所以,现在可以总结出:
层i的起始编号l_i =k*m^i + (2 -m)*(m^i -1)/(m-1 )
层i的结束编号r_i =k*m^i + (m^i -1)/(m-1 )
这样,每一层的起始和结束都可以通过这两个公式计算得到。
现在,当计算每一层的时候,我们需要判断该层是否存在:
如果 l_i >n → 该层不存在,退出循环。
否则,该层存在,那么:
如果 r_i <=n → 该层节点数目为m^i → 加到总ans上。
否则 → 该层数目为n - l_i +1 → 加到ans,并退出循环。
否则,继续到下一层。
现在,如何计算每一层的l_i和r_i?
例如,当i=0时:
l_0 =k*1 + (2 -m)*(1-1)/(m-1 ) =k →正确.
r_0 =k*1 + (1-1)/(m-1 )=k →正确.
i=1:
l_1 =k*m + (2 -m)*(m-1)/(m-1 )=k m +2 -m →正确.
r_1 =k m + (m-1)/(m-1 )=k m +1 →正确.
所以,这两个公式是正确的。
那么,现在问题转化为,如何快速计算每个i的l_i和r_i,并根据n的情况累加ans。
这里,可以逐步计算每一层的l_i和r_i,直到l_i超过n为止。对于每个i,从0开始:
初始化 ans=0.
current_layer =0.
while true:
计算 l_i =k*m^i + (2-m)*(m^i -1)/(m-1 )
r_i =k*m^i + (m^i -1)/(m-1 )
如果 l_i >n → break.
否则:
如果 r_i <=n → ans += m^i.
否则 → ans += max(0, n - l_i +1 ), break.
然后,current_layer +=1 → i递增.
直到循环结束.
现在,问题是如何高效计算m^i和相关的项。对于大i来说,m^i可能很大,超出long long的范围。但题目中的变量是int,但是原代码中使用的是#define int long long。所以,n可以是很大的值,比如1e18。那么,在计算m^i时,当i过大时,可能导致溢出。例如,当m=1e9,i=20时,m^i=1e180,远远超过long long的范围。这时候,如何处理?
因此,在计算过程中,必须及时判断是否溢出。例如,当计算m^i的时候,如果在某一步骤发现k*m^i已经超过了n的范围,或者可能导致溢出,那么可以直接终止循环。
或者,可以预先计算,当i达到某个值时,l_i已经超过n,从而停止。
所以,在代码实现时,需要注意避免溢出。例如,当计算l_i和r_i时,如果当前结果已经超过n,或者超过了可能的最大值(比如,当m^i超过某个阈值时,就可以提前终止)。
现在,具体如何实现:
初始化变量:
ans =0
current_level =0
while True:
计算 m^i → 可以用pow,但因为i递增,可以每次乘以m。例如,维护一个变量pow_m = m^current_level.
同时,计算 (m^i -1)/(m-1)。可以预先计算该值,比如sum_m = (m^i -1)/(m-1) =1 +m +m^2 +...+m^{i-1} → 这是等比数列的和。例如,sum_m可以维护为前一个sum_m *m +1。因为:
sum_m for i=0 → (m^0 -1)/(m-1) =0?或者i是当前层数,那么sum_m for层i是 (m^i -1)/(m-1).
比如,对于层i来说:
sum_m = (m^{i} -1)/(m-1)
例如,i=0 → sum_m =0.
i=1 → sum_m =1 → (m-1)/(m-1)=1.
i=2 → sum_m =1 +m = (m²-1)/(m-1) =m+1.
所以,sum_m可以递推计算:
初始sum_m_prev =0(i=0时)
每次循环,sum_m = sum_m_prev *m +1 → 比如:
i=0 → sum_m_prev=0 → sum_m=0*m +1=1? 这里可能我的初始值有问题。或者,应该重新考虑。
sum_m for层i的计算:
sum_m = (m^i -1)/(m-1 )
例如,当i=0 → sum_m=0.
i=1 → sum_m=1.
i=2 → sum_m=1 +m.
i=3 → sum_m=1 +m +m².
所以,sum_m可以通过递推式:sum_m_i = sum_m_{i-1} *m +1 ?
例如,i=1:
sum_m_0=0 → sum_m_1=0*m +1=1.
i=2:
sum_m_1=1 → sum_m_2 =1*m +1 =m+1.
i=3:
sum_m_3 = (m+1)*m +1= m² +m +1.
这与正确的结果相符。所以,sum_m的递推式是正确的。
同样,另一个项是 (2 -m) * sum_m,即(2 -m) * (m^i -1)/(m-1 )
因此,可以维护以下变量:
pow_m → m^i.
sum_m → (m^i -1)/(m-1 )
term → (2 -m) * sum_m.
那么,l_i =k * pow_m + term.
r_i =k * pow_m + sum_m.
所以,在代码中,可以维护这些变量:
初始化:
pow_m =1 → m^0 =1.
sum_m =0 → (m^0 -1)/(m-1 )=0.
term = (2 -m) * sum_m =0.
l_i =k * pow_m + term =k*1 +0 =k.
r_i =k * pow_m + sum_m =k*1 +0 =k.
然后,对于每一层i(current_level从0开始):
计算 l_i和r_i:
l =k * pow_m + (2 -m)*sum_m;
r =k * pow_m + sum_m;
然后判断:
如果 l >n → 退出循环.
否则:
如果 r <=n → ans += m^i →即ans += pow_m.
否则 → ans += (n -l +1), 退出循环.
然后,进入下一层:
pow_m *=m → 变成m^{i+1}
sum_m = sum_m *m +1 → 变成 (m^{i+1} -1)/(m-1 )
current_level +=1.
重复循环.
但需要注意,当m=1时的情况。因为当m=1时,等比数列的和的公式 (m^i -1)/(m-1 ) 会出现除以零的情况。因此,必须单独处理m=1的情况。
例如,当m=1时,每个节点只能有一个子节点。那原问题中的child函数是k*1 +2 -1 =k+1。所以,每个节点的子节点只有一个,即k+1。那么,整个子树的节点数目是连续的,从k到某个最大值。例如,子树k的节点数目是最大的h,使得k +h <=n。即数目为h+1。或者,更准确地说,当m=1时,每个节点只有1个子节点,因此子树的结构是链状的,每个节点的子节点是k+1,k+2等。因此,以k为根的子树的节点数目是最大的数目t+1,其中t是满足k +t <=n的最大t。例如,数目为n -k +1,只要k <=n。否则数目为0.
所以,当m=1时,子树的节点数目是max(0, n -k +1).
所以,在代码中,必须处理m=1的特殊情况,否则在计算sum_m的时候会出现除以零的情况。
综上,算法的大致步骤如下:
如果 m ==1:
如果k >n →0.
否则 →数目是n -k +1.
否则:
初始化 current_level=0, pow_m=1, sum_m=0, ans=0.
while True:
l =k*pow_m + (2 -m)*sum_m.
r =k*pow_m + sum_m.
if l >n → break.
if r >n → ans +=n -l +1 → break.
else → ans += pow_m.
current_level +=1.
next_pow_m = pow_m *m.
next_sum_m = sum_m *m +1.
//检查是否溢出,比如当pow_m *m 超过LLONG_MAX时,会导致溢出,此时下一层的l将远大于n,所以可以直接break.
//假设这里的n的范围是可以用long long表示的,那么当pow_m超过n /m 时,下一个pow_m可能溢出,此时可以提前终止。
if next_pow_m >n/m → 可能溢出,那么下一层的l肯定大于n → break.
else →更新pow_m = next_pow_m, sum_m = next_sum_m.
return ans.
这样,时间复杂度是O(log_m (n)),因为每次current_level增加,pow_m乘以m,直到超过n。这样,循环次数最多是 log_m(n)次。
现在,将这个思路转化为代码。
原代码中的solve函数使用BFS,现在需要替换成数学计算的方式。
原函数的参数是n, m, k。我们需要返回ans的值,即k的子树中的节点数目。
考虑特殊情况:
如果k <1或者k>n →ans=0.
否则:
当m=1 →ans = max(0, n -k +1).
否则:
按照上述算法计算ans.
现在,如何判断溢出?
例如,当计算pow_m *m时,可能超过LLONG_MAX的值,导致溢出。在C++中,可以检测是否溢出。例如,在每次计算next_pow_m = pow_m *m时,如果pow_m > LLONG_MAX /m,那么就会溢出。此时,下一层的l_i将非常大,超过n,所以可以直接break循环。
所以,在代码中,每层循环中,当计算下一层的pow_m和sum_m时,先检查是否会溢出:
next_pow_m = pow_m *m.
if next_pow_m > LLONG_MAX /m →这可能需要调整条件。比如,判断pow_m是否超过 LLONG_MAX/m →如果pow_m > LLONG_MAX/m →则相乘会溢出。否则,不会。
所以,在每次循环中,在更新pow_m和sum_m之前,检查pow_m是否超过 (LLONG_MAX -1)/m。如果是的话,下一层的pow_m *m将溢出,此时可以提前终止循环。
例如,在代码中:
if (pow_m > LLONG_MAX / m) {
// 下一层的pow_m = pow_m *m 会溢出,所以无法继续计算,退出循环.
break;
}
否则,可以安全地计算next_pow_m = pow_m *m.
同样的,sum_m的计算可能溢出,但sum_m的溢出可能导致计算结果错误。然而,在sum_m溢出时,其值可能变得很大,导致下一层的l_i和r_i也很大,超过n,从而退出循环。所以,可以不用处理sum_m的溢出,因为它不影响最终结果的正确性,只是可能提前终止循环。
综上,代码的solve函数可以改写为:
void solve(int n, int m, int k) {
if (k <1 || k>n) {
cout <<0<<endl;
return;
}
if (m ==1) {
// 每个节点只有1个子节点,子树是链式结构,节点数目是n -k +1.
int ans = max(0LL, n -k +1);
cout << ans <<endl;
return;
}
int ans =0;
int current_level =0;
long long pow_m =1; // m^current_level
long long sum_m =0; // (m^current_level -1)/(m-1)
while (true) {
long long l = k * pow_m + (2 -m) * sum_m;
long long r = k * pow_m + sum_m;
if (l >n) {
break;
}
if (r >n) {
ans += (n - l +1);
break;
} else {
ans += pow_m;
}
// 计算下一层.
// 检查pow_m *m 是否溢出.
if (pow_m > LLONG_MAX / m) {
break;
}
long long next_pow_m = pow_m * m;
long long next_sum_m = sum_m * m +1;
pow_m = next_pow_m;
sum_m = next_sum_m;
current_level++;
}
cout << ans <<endl;
}
但需要注意,当m=0时的情况?根据问题描述,m是完全m叉树的度数,所以m应该≥1。原代码中的输入可能保证m≥2,但根据题目输入,可能需要处理m=0的情况?但根据常规,m叉树的度数至少是2,所以可能原题的输入中m≥2。或者,原代码中的solve函数可能没有处理m=0的情况。例如,原代码中的solve函数中,当m=1时,可能进入其他分支?
原问题中的代码中的solve函数中,当m=1时,会调用child函数。例如,当k=1,m=1时,child函数返回1*1 +2-1=2。所以,每个节点的第一个子节点是k+1。这与m=1的情况相符。但是,当m=1时,原代码的BFS会遍历k的子节点,即k+1,然后每个子节点的子节点是k+2,依此类推,直到超过n。这样的BFS的时间复杂度是O(ans),即O(n)的,当ans很大时,会很慢。而数学方法可以将时间复杂度降为O(1)。因此,在优化代码中,必须处理m=1的情况,单独处理。
现在,测试几个例子,验证算法的正确性。
例子1:
输入:n=4,m=3,k=1.
正确输出:4(节点1的子树包括1,2,3,4)。
原代码中的BFS会计算出4。数学方法:
当m=3时,计算:
初始pow_m=1,sum_m=0.
current_level=0:
l=1*1 + (2-3)*0=1.
r=1*1 +0=1.
1<=4,r=1<=4 →ans +=1.
下一层:
pow_m=3, sum_m=1.
current_level=1:
l=1*3 + (2-3)*1=3-1=2.
r=1*3 +1=4.
因为2<=4,r=4 <=4 →ans +=3^1=3. ans=1+3=4.
下一层:
pow_m=3*3=9. sum_m=1*3 +1=4.
检查pow_m=9是否超过LLONG_MAX/m(假设LLONG_MAX足够大,这里可能测试值较小)。
计算l=1*9 + (2-3)*4=9-4=5. 5>4 →break.
总ans=4.
正确。
例子2:
n=7, m=2, k=2.
原树结构:
1的子节点是2,3.
2的子节点是4,5.
3的子节点是6,7.
k=2的子树包括4,5,以及它们的子节点。假设n=7,那么4的子节点是8,9,但8>7,所以子树k=2的节点数目是2(4,5)?或者可能我理解错了?
原代码中,当k=2,m=2,n=7时,solve会执行BFS:
队列初始为2.
出队2,将其子节点计算为4,5 →入队4、5 →ans+=1(当前队列元素处理后ans增加?或者原代码中的ans计数方式可能有问题)。
原代码中的ans初始化为0。进入循环:
队列非空,取出队首元素(k=2),然后遍历所有m个子节点:
对于j=0到1:
计算子节点a=2*2 +2-2=4 +0=4 +j →4,5.
a=4和5都<=7,所以入队。然后ans++两次?或者原代码中的ans是每次循环处理队列中的一个元素时ans增加?
原代码中的循环结构:
每次处理队列中的一个元素(比如队列中的队首元素):
当处理队首元素时,将它的m个子节点入队。然后ans++。然后pop该元素。
例如,初始队列中是k=2.
循环开始时,队列不为空:
处理队首元素k=2:
生成子节点4,5 →入队,此时队列中有4、5.
然后ans++ →ans=1.
然后pop该元素。
之后,队列中有4、5.
然后处理4的子节点:
子节点是4*2 +2-2=8-0=8 → a=8+0=8, 8+1=9 →都不超过n=7,所以不入队。ans++ →ans=2.
处理完4后,队列中有5.
处理5的子节点:5*2 +2-2=10-0=10 →a=10+0=10,10+1=11>7 →不入队。ans++ →ans=3.
最后,队列为空。总ans=3.
但正确的子树节点数目是2(节点2的子节点是4、5,而它们的子节点超过7,所以总共有1(节点2) +2(4、5)?或者原代码中的ans计算方式是否错误?
原代码中的ans初始为0,然后每次处理队列中的一个元素时,ans++。例如,初始队列中有k=2,处理后ans=1,然后队列中有4、5。然后处理4,ans=2,队列中有5。处理5,ans=3。所以原代码的输出是3。而正确的子树数目应该是3吗?
节点k=2的子树包括节点2、4、5。所以数目是3。是的。那数学方法如何计算?
当m=2,k=2,n=7.
算法:
m !=1.
初始化 pow_m=1, sum_m=0.
current_level=0:
l=2*1 + (2-2)*0=2.
r=2*1 +0=2.
l=2<=7.
r=2 <=7 →ans +=1.
current_level=1:
pow_m=2, sum_m=1.
l=2*2 +0*1=4+0=4.
r=2*2 +1=5.
4<=7,r=5<=7 →ans +=2 →ans=3.
current_level=2:
pow_m=4, sum_m=3.
l=2*4 +0*3=8.
r=2*4 +3=11.
l=8>7 →break.
总ans=3 →正确.
所以,数学方法正确。
再来看原代码中的计算是否正确。原代码的BFS确实计算的是包括根节点在内的所有节点数目。例如,ans初始为0,每处理一个节点ans就加1。所以,当k=2的子树有3个节点时,原代码输出3,而数学方法同样得到3,结果正确。
这表明数学方法是正确的。
另一个例子:
n=10,m=3,k=2.
原树的结构:
节点1的子节点是2,3,4.
节点2的子节点是5,6,7.
节点3的子节点是8,9,10.
节点4的子节点将超过10.
k=2的子树包括节点2,5,6,7,以及它们的子节点。假设n=10.
子节点5的子节点是5*3+2-3=15+2-3=14?或者需要重新计算:
child(k,m) =k*m +2 -m →当k=5,m=3,child=5*3 +2-3=15-1=14 →子节点是14,15,16。这些超过10,所以节点5的子节点不在树中。同理,节点6的子节点是6*3 +2-3=18-1=17 →17,18,19>10。节点7的子节点是7*3 +2-3=21-1=20>10。所以k=2的子树包括节点2、5、6、7 →总共有4个节点。
原代码的BFS处理过程:
队列初始为2 →ans=0.
处理队列中的2 →子节点5、6、7 →入队。ans++ →ans=1。队列中有5、6、7.
处理5 →子节点14、15、16>10 →不入队。ans++ →ans=2.
处理6 →子节点17、18、19>10 →ans++ →ans=3.
处理7 →子节点20>10 →ans++ →ans=4.
队列为空。总ans=4 →正确。
数学方法:
m=3,k=2,n=10.
初始化:
pow_m=1, sum_m=0.
current_level=0:
l=2*1 + (2-3)*0=2.
r=2*1+0=2.
ans +=1 →ans=1.
current_level=1:
pow_m=3, sum_m=1.
l=2*3 + (-1)*1=6-1=5.
r=2*3+1=7.
5<=10,r=7<=10 →ans +=3^1=3 →ans=4.
current_level=2:
pow_m=3*3=9.
sum_m=1*3 +1=4.
l=2*9 + (-1)*4=18-4=14.
14>10 →break.
总ans=4 →正确.
这表明算法正确。
综上,数学方法的逻辑是正确的,可以替换原代码中的BFS部分,从而将时间复杂度从O(ans)(可能很大)降低到O(log_m (n)),这在大部分情况下非常高效。
现在,将原代码的solve函数替换为此数学方法,并处理m=1的特殊情况,即可完成优化。
最终的代码修改如下:
原代码中的solve函数将被替换为数学计算版本,而主函数保持结构不变。同时,需要注意数据类型,使用long long防止溢出。
综上,优化后的代码如下:</think>以下是对代码的时间复杂度优化方案,将BFS遍历改为数学公式计算,时间复杂度从$O(N)$降低到$O(\log N)$:
```cpp
#include <iostream>
#include <climits>
#define int long long
using namespace std;
void solve(int n, int m, int k) {
if (k < 1 || k > n) {
cout << 0 << endl;
return;
}
if (m == 1) { // 处理m=1的特殊情况
cout << max(0LL, n - k + 1) << endl;
return;
}
int ans = 0;
int current_level = 0;
long long pow_m = 1; // m^current_level
long long sum_m = 0; // (m^current_level -1)/(m-1)
while (true) {
long long l = k * pow_m + (2 - m) * sum_m;
long long r = k * pow_m + sum_m;
if (l > n) break;
if (r > n) {
ans += (n - l + 1);
break;
} else {
ans += pow_m;
}
// 检查是否溢出,提前终止
if (pow_m > LLONG_MAX / m) break;
long long next_pow_m = pow_m * m;
long long next_sum_m = sum_m * m + 1;
pow_m = next_pow_m;
sum_m = next_sum_m;
current_level++;
}
cout << ans << endl;
}
signed main() {
int t;
cin >> t;
while (t--) {
int n, m, k;
cin >> n >> m >> k;
solve(n, m, k);
}
return 0;
}
```
### 优化点说明
1. **数学建模替代遍历**
原代码用BFS逐层遍历子树节点,时间复杂度为$O(N)$。通过分析完全m叉树的结构特征,直接计算每层的**起始编号**和**结束编号**,利用等比数列求和公式在$O(\log N)$时间内得出结果。
2. **边界条件处理**
- 当$m=1$时退化成链式结构,直接计算区间长度$n-k+1$。
- 处理数值溢出问题,当检测到$m^i$可能超过`long long`范围时提前终止。
3. **复杂度分析**
每轮循环计算一层节点,最多循环$\log_m N$次,时间复杂度优化至$O(\log N)$,可处理$N \leq 10^{18}$的大规模数据。
阅读全文
相关推荐


















