题目
题意概要
对于一棵无限大的完全二叉树,节点
x
x
x 的左儿子为
2
x
2x
2x,右儿子为
2
x
+
1
2x+1
2x+1 。
请求出有多少个点对 a , b ( a ⩽ b ) a,b\;(a\leqslant b) a,b(a⩽b) 满足 a , b a,b a,b 的树上路径的点编号之和为 s s s 。
数据范围与提示
s
⩽
1
0
15
s\leqslant 10^{15}
s⩽1015 。
思路
从一个 s i m p l e v e r s i o n \rm simple\;version simpleversion 想起——只有一条链。
很容易发现,一个长度为 h h h 的链,以 x x x 作为上端点,点权和最小是 ∑ j = 0 h − 1 2 j x = ( 2 h − 1 ) ⋅ x \sum_{j=0}^{h-1}2^jx=(2^h-1)\cdot x ∑j=0h−12jx=(2h−1)⋅x,即一直往左儿子走的情况。
显然 x x x 为上端点的任意情况都劣于 x + 1 x+1 x+1 为上端点的情况。所以实际上 每个上端点对应的点权和区间是不交的。
如果要具体一点,就是设 t i = 1 t_i=1 ti=1 表示自底向上第 i i i 个位置选择向右走。那么这个选择提供的权值变化是 ∑ j = 0 i − 1 2 j = 2 i − 1 \sum_{j=0}^{i-1}2^j=2^i-1 ∑j=0i−12j=2i−1,因为第 j ( j ⩽ i ) j\;(j\leqslant i) j(j⩽i) 个位置会被迫移动 2 i − j 2^{i-j} 2i−j 个位置。此时即使取 t t t 全部为 1 1 1 也不能达到 ( x + 1 ) (x+1) (x+1) 对应的最小点权和。
类似地,如果 x x x 为 a , b a,b a,b 的 l c a \rm lca lca,两边的长度分别为 l , r l,r l,r,那么点权和最小是 ( 2 l − 1 + 2 r − 1 − 1 ) ⋅ x + ( 2 r − 1 − 1 ) (2^l-1+2^r-1-1)\cdot x+(2^{r-1}-1) (2l−1+2r−1−1)⋅x+(2r−1−1),即两条路径的权值和相加,减去 x x x 重复计算了一次。你会发现它还是满足 区间不交 的性质……
所以枚举 l , r l,r l,r 之后, x x x 是 唯一的。其次只需要求出两边对应的 t t t 序列即可,类似于一个背包问题。背包容量很大,怎么办?考虑 枚举个数 来填补 − 1 -1 −1,这样物品权值变为 2 i 2^i 2i 便可以直接按照 s s s 的要求选择。
枚举 l , r l,r l,r 和个数,做一个数位 d p \tt dp dp,三个维度分别是考虑到第几位、已选个数、进位(零或一)。时间复杂度 O ( log 5 s ) \mathcal O(\log^5s) O(log5s),非常离谱的复杂度。实际运行非常快,可以通过。
有一个优化。枚举个数对目标 s s s 的影响很小,即 s ′ = s + c n t s'=s+cnt s′=s+cnt 在前 log log s \log\log s loglogs 位可能有明显的变化,但是后面至多变化一次。那么我们可以从后往前做 d p \tt dp dp,定义不变。后面的二进制位如果不变,就完全不必重新求 d p \tt dp dp 数组。此时复杂度是 O ( log 4 s log log s ) \mathcal O(\log^4s\log\log s) O(log4sloglogs),稍微靠谱一点。
代码
当然并没有加上那个优化。
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long int_;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
inline int readint(){
int a = 0, c = getchar(), f = 1;
for(; '0'>c||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(int x){
if(x > 9) writeint(x/10);
putchar((x-x/10*10)^48);
}
const int MAXN = 105;
int_ dp[2][MAXN][2];
const int coe[3][3] = {{1,0,0},{1,1,0},{1,2,1}};
int main(){
long long s; scanf("%lld",&s);
int n = 1; while(s>>n) ++ n;
int_ ans = 0; // int_ needed
rep(l,1,n) rep(r,1,n){
int_ k = s-(1ll<<r>>1)+1;
int_ x = k/((1ll<<l)+(1ll<<r)-3);
if(x <= 0) continue; // invalid
k -= x*((1ll<<l)+(1ll<<r)-3);
const int max_cnt = (l == 1 ? 0 : (l-2)) + (r == 1 ? 0 : (r-2));
if(k+max_cnt < 0) continue; // not achievable
int cnt = static_cast<int>((k > 0) ? 0 : (-k));
for(k+=cnt; cnt<=max_cnt; ++cnt,++k){
if(k&1) continue; // no way to fix it
if(!k){ ++ ans; continue; } // choose nothing
int len = 0; while(k>>len) ++ len;
dp[0][0][0] = 1, dp[0][0][1] = 0;
int now = 0, lst; // count of choices
for(int i=1,fr=0; i<len; ++i,fr^=1){
lst = now; now += (i <= l-2) + (i <= r-2);
memset(dp[i&1],0,(now+1)*2<<3);
rep(j,0,lst) rep(d,0,1) rep(a,0,now-lst)
if((d^a^(k>>i)^1)&1) // match bit
dp[i&1][j+a][(d+a)>>1] +=
coe[now-lst][a]*dp[fr][j][d];
}
if(now >= cnt) // achievable
ans += dp[(len&1)^1][cnt][0];
}
}
printf("%lld\n",ans);
return 0;
}
后记
这个 区间不交 的性质真的神奇。如果非要说原因的话——权值是关于 x x x 的一次函数。就像这道题就是直接维护函数。