【洛谷7143】[THUPC2021 初赛] 线段树(动态规划)
- 对于一个长度为\(n\)的序列建出一棵标准线段树,定义一个区间的权值为至少用线段树上多少个节点才能恰好表示出这个区间。
- 求所有区间的权值之和。
- 数据组数\(\le10^3\),\(n\le10^{18}\)
计算点的贡献
考虑一个区间对应的节点个数,就是把这个区间扔到线段树上时,节点裂开的次数\(+1\)(显然一次裂开会增加一个点,\(+1\)是因为最初有一个点)。
所以我们考虑计算每个点裂开的方案数,这就等价于求有多少个区间跨越这个节点的中点且不完全包含这个节点。
动态规划
假设一个节点在原序列中对应区间的左边有\(l\)个位置,右边有\(r\)个位置,并设这个点两个儿子对应区间大小分别为\(ls\)和\(rs\)。
那么能使得这个点裂开的区间可以分成三类:被这个点包含、完全包含左儿子、完全包含右儿子。
总计算式就应该是\((ls\times rs-1)+l\times (rs-1)+r\times (ls-1)\)。
现在我们想\(DP\)这玩意,\(ls\)和\(rs\)是可以根据当前节点对应区间大小\(n\)直接计算的,且由于这是一棵标准线段树,可能的区间大小只有\(O(logn)\)种,可以计入状态。但如果我们把\(l\)和\(r\)一同维护进状态里显然爆炸。
但根据我们之前总计算式的形式,发现一个点的贡献可以表示为\(a+l\times b+r\times c\)。
所以我们设\(F_n,L_n,R_n\)表示一个大小为\(n\)的节点整棵子树的贡献为\(F_n+l\times L_n+r\times R_n\)。
从左子节点向上转移的贡献为\(F_{ls}+l\times L_{ls}+(r+rs)\times R_{ls}\),即\((F_{ls}+rs\times R_{ls})+l\times L_{ls}+r\times R_{ls}\),依然可以保持这个形式不变,右子节点向上转移同理。
代码:\(O(Tlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define LL long long
#define X 1000000007
using namespace std;
LL n;map<LL,int> F,L,R;I void Solve(Con LL& n)//求F[n],L[n],R[n]
{
if(F.count(n)) return;LL ls=n+1>>1,rs=n-ls;Solve(ls),Solve(rs);//记忆化;递归
F[n]=(F[ls]+F[rs]+rs%X*R[ls]+ls%X*L[rs]+(ls%X)*(rs%X)-1)%X,L[n]=(L[ls]+L[rs]+rs-1)%X,R[n]=(R[ls]+R[rs]+ls-1)%X;//从子节点转移并加上当前点贡献
}
int main()
{
RI Tt;scanf("%d",&Tt),F[1]=L[1]=R[1]=0;//初始化n=1的边界情况
W(Tt--) scanf("%lld",&n),Solve(n),printf("%d\n",(F[n]+(n%X)*(n%X+1)/2)%X);return 0;//注意对每种方案加上1
}
待到再迷茫时回头望,所有脚印会发出光芒