话说这题出题人给的题解看了半天看不大懂啊……
还是抄 看了好多AC代码才搞明白
题目传送门
首先这道题的题面给出了一个easy版本的问题,我们先从这一问题开始。
读完题目后,我们略一思考就可发现,随着x的减少,k是会不断的增加的,这显然是一个单调的函数关系,因此很容易想到二分。在easy版本中k固定的情况下,二分的区间就是1~n,每次check时,我们先找出当前深度最深的点,向上跳x个距离到达某一结点u,将其设为关键点,然后将以u点为根的子树打上标记,表示这颗子树已经被“处理”了。这样每次重复上述过程,直到整棵树都已经被标记,这样我们就可以得到一个关键点的数量k’,将k’与k进行比较即可。当然,根节点必须是关键点。
实际代码实现可能需要一些细节,就不深入了。
接下来进入正题。
这道题将k改为了一个从1~n的值,如果我们依然像上述做法一样固定k,二分x的话,复杂度会比较高,因此我们换一种思路。
我们发现,无论k是多少,x都只会落在1~n的范围内,因此,我们只需要枚举x,然后按上述方法求出每一个x对应的k,用数组将其记录下来。由于一个k可能对应有不同的x,所以我们还需要取一个x的最小值。
不过实现起来还有亿点点细节。
寻找最大值、向上跳x步以及标记树上结点的操作都必须是log级别,不然会T,因此我们通过dfs序将这棵树映射到数组上,并用线段树维护其最深的结点和标记情况,寻找祖先的方法则是倍增。
不了解dfs序和倍增的小伙伴自行百度。
下面上代码
#include
using namespace std;
const int maxn = 2e5+10;
struct edge{
int to,nxt;
}e[maxn<<1];
///线段树,col为懒惰标记,0表示未标记,1表示已标记,2表示还原标记
struct tr{
int mx,mx2,col;///这里存两个最深的结点编号方便还原
}tree[maxn<<3];
int head[maxn];
int parent[maxn][30];
int newIdx[2][maxn];
int ans[maxn];
int dfn[maxn<<1];
int Log2[maxn],depth[maxn];
int cnt,tot,n,m;
void addEdge(int u,int v){
e[++cnt].to = v;
e[cnt].nxt = head[u];
head[u] = cnt;
}
inline void pushUp(int u){
tree[u].mx = depth[tree[u<<1].mx]>depth[tree[u<<1|1].mx]?tree[u<<1].mx:tree[u<<1|1].mx;
}
void build(int l,int r,int u){
if(l==r){
tree[u].mx = dfn[l];
tree[u].col = 0;
tree[u].mx2 = tree[u].mx;
return;
}
int mid = (l+r)>>1;
build(l,mid,u<<1);
build(mid+1,r,u<<1|1);
pushUp(u);
tree[u].mx2 = tree[u].mx;
}
void dfs(int u,int fa){///处理dfs序和倍增
dfn[++tot] = u;
newIdx[0][u] = tot;
depth[u] = depth[fa]+1;
parent[u][0] = fa;
for(int i=1;i<=Log2[depth[u]]-1;++i)
parent[u][i] = parent[parent[u][i-1]][i-1];
for(int i=head[u];i;i=e[i].nxt){
int v = e[i].to;
if(v==fa) continue;
dfs(v,u);
}
dfn[++tot] = u;
newIdx[1][u] = tot;
}
int find(int u,int x){///倍增寻找祖先
int a = u;
if(x==0) return a;
for(int i=Log2[x]-1;i>=0;--i){
if((1<<i)<x&&parent[a][i]){
a = parent[a][i];
x -= (1<<i);
}
}
return parent[a][0];
}
void pushDown(int u){
int col = tree[u].col;
if(!col) return;
if(col==1){
tree[u<<1].col = tree[u<<1|1].col = col;
tree[u<<1].mx = tree[u<<1|1].mx = 0;
tree[u].col = 0;
}
else if(col==2){
tree[u<<1].col = tree[u<<1|1].col = col;
tree[u<<1].mx = tree[u<<1].mx2;
tree[u<<1|1].mx = tree[u<<1|1].mx2;
tree[u].col = 0;
}
}
void modify(int L,int R,int l,int r,int u){
if(L>=l&&R<=r){
tree[u].col = 1;
tree[u].mx = 0;
return;
}
pushDown(u);
int mid = (L+R)>>1;
if(mid>=l) modify(L,mid,l,r,u<<1);
if(mid<r) modify(mid+1,R,l,r,u<<1|1);
pushUp(u);
}
int solve(int x){
tree[1].mx = tree[1].mx2;
tree[1].col = 2;
int start,u,cnt = 0;
while(1){
start = tree[1].mx;
u = find(start,x);
modify(1,tot,newIdx[0][u],newIdx[1][u],1);
cnt ++;
if(u==1) break;
}
return cnt;
}
int main()
{
while(scanf("%d",&n)!=EOF){
///记得务必初始化干净
int res = 0;
memset(parent,0,sizeof(parent));
cnt = tot = depth[0] = 0;
for(int i=1;i<=n;++i){
head[i] = 0;
newIdx[0][i] = newIdx[1][i] = 0;
depth[i] = 0;
Log2[i] = Log2[i-1]+(1<<Log2[i-1]==i);
}
for(int i=1;i<n;++i){
int u,v,w;
scanf("%d %d %d",&u,&v,&w);
addEdge(u,v);
addEdge(v,u);
}
dfs(1,1);
build(1,tot,1);
for(int i=1;i<=n;++i){
ans[i] = n+1;
}
///枚举x,计算k,用x更新k
///因为是反向枚举,所以最后得到的一定是最小值
for(int i=n;i>=0;--i){
ans[solve(i)] = i;
}
///由于枚举x得到的k是最小的,有些k会没有被更新到,所以要再判一次
for(int i=2;i<=n;++i){
if(ans[i]==n+1) ans[i] = ans[i-1];
}
for(int i=1;i<=n;++i){
res += ans[i];
}
printf("%d\n",res);
}
return 0;
}
下面还剩下最后一个问题,关于这个算法的复杂度。
通过以上代码我们可以看出,对于每一个枚举的x,我们都要进行k次循环,而每次循环内部进行的更改都是logn的,所以最后算法的复杂度为
∑ k k l o g n \sum_{k}klogn k∑klogn
假设一条链的深度为d,我们通过画图可以发现:
x = d时,k = 1
x = d-1 ~ d/2时,k = 2
x = d/2 - 1 ~ d/3时,k = 3
……
总结一下就是:
k = ⌊ d x ⌋ ( + 1 ) k=\lfloor \frac{d}{x} \rfloor (+1) k=⌊xd⌋(+1)
后面的+1根据情况,能整除时不用,不能整除时就要+1
于是,算法的复杂度就变成了
∑ i = 1 n n i l o g n \sum_{i=1}^n \frac{n}{i}logn i=1∑ninlogn
中间出现了调和级数
查百度可以知道,调和级数的近似值为ln(x+1)
于是算法的复杂度近似为n(logn)^2
如果题解中有什么错误欢迎指出