一年前学的东西,但是直到最近几天才想明白。
性质1:选的路径端点一定都是叶子
证明显然,因为不在叶子把它拓展到叶子边权和显然会增大。
我们先不考虑一定包含 x x x的限制。
设 c n t cnt cnt为叶子数。
那么如果 2 y < = c n t 2y<=cnt 2y<=cnt 最终的路径端点一定是由 2 y 2y 2y个叶子构成
.
否则显然可以存在一种方案取到所有的边。
性质2:对于任意一个选择的叶子集合,总有一种方法使得选出的边为这些叶子的虚树的边。
证明的话,如果某两条路径不相交,我们换一下端点就可以变得相交。这样最终所有路径都会相交,而所有路径的并自然是虚树的边集了。
考虑有 x x x怎么办,首先我们强制让 x x x被选,也就是说再选 2 y − 1 2y-1 2y−1个叶子。
同时把 x x x作为根。那么虚树就是叶子到根的路径的并。
也就是说,我们要求 2 y − 1 2y-1 2y−1个叶子,使得他们到根的路径并的权值之和最大。
性质3:我们把树做带权长链剖分,那么答案就是选前 2 y − 1 2y-1 2y−1大的链。
首先这样选一定是最大的,并且同时因为长链剖分的性质,即每次挑最长的儿子,那么一定满足,如果 x x x所在链被选了,那么 x x x的链顶的父亲所在链也一定被选了。
但是有一个问题就是因为我们是强制 x x x选了,那么可能 x x x不在本来的虚树里,那么问题来了,是否有一个点一定在原本虚树里呢?。
性质4:直径端点一定在答案里。
也很对,因为长剖后第一条选的链肯定是直径端点。
综上所述,我们得到了一个一定出现在虚树中的点。
这样,我们就可以把直径端点作为树的根,因为端点有两个,所以分别以两个为根做一遍取最大值。
然后我们分析怎么保证 x x x一定被选。
不妨先把前 2 y − 1 2y-1 2y−1大的链选了,-1是因为根也是叶子。
预处理 r k x rk_x rkx表示 x x x所在链的排名,也就是说第几大的。
如果 x x x已经被包含了,即 r k x ≤ 2 y − 1 rk_x\leq 2y-1 rkx≤2y−1,那么直接就好了。
否则,有两种处理办法
1.从 x x x子树内选一个叶子打通到跟的路径,同时删掉排名为 2 y − 2 2y-2 2y−2的路径。
2.求出 x x x向上走第一个被虚树覆盖的节点,不妨设为 z z z,可以倍增出来。
如果 z z z子树内只选了一个,那就把原来选的删掉换成 x x x子树内的叶子就好了。
如果不止一个,那么一定有一条链的权值较小,就会被第一种情况考虑到了。
综上所述,就在 O ( n l o g n ) O(nlogn) O(nlogn)的时间内解决了。
#include
using namespace std;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
const int N = 1e5+7;
int n,m;
struct edge
{
int y,v,next;
}e[2*N];
int link[N],t=0;
void add(int x,int y,int v)
{
e[++t].y=y;
e[t].v=v;
e[t].next=link[x];
link[x]=t;
}
struct machine
{
int dis[N],root;
int fa[N][18];
int dep[N],mxdep[N],son[N];
void dfs(int x,int pre)
{
fa[x][0]=pre;
for(int k=1;fa[x][k-1];k++)
fa[x][k]=fa[fa[x][k-1]][k-1];
for(int i=link[x];i;i=e[i].next)
{
int y=e[i].y;
if(y==pre)continue;
dep[y]=mxdep[y]=dep[x]+e[i].v;
dfs(y,x);
if(mxdep[y]>mxdep[x])
{
mxdep[x]=mxdep[y];
son[x]=y;
}
}
}
int top[N];
void Exdfs(int x,int topth)
{
top[x]=topth;
if(!son[x])return;
Exdfs(son[x],topth);
for(int i=link[x];i;i=e[i].next)
{
int y=e[i].y;
if(y==fa[x][0]||y==son[x])continue;
Exdfs(y,y);
}
}
int cnt=0;
#define PII pair<int,int>
#define mk(x,y) make_pair(x,y)
#define X(x) x.first
#define Y(x) x.second
PII chain[N];
int rk[N];
int sum[N];
void Build()
{
dfs(root,0);
Exdfs(root,root);
for(int i=1;i<=n;i++)
if(top[i]==i)
chain[++cnt]=mk(mxdep[i]-dep[fa[i][0]],i);
sort(chain+1,chain+cnt+1);
reverse(chain+1,chain+cnt+1);
for(int i=1;i<=cnt;i++)
rk[Y(chain[i])]=i;
for(int i=1;i<=n;i++)
rk[i]=rk[top[i]];
for(int i=1;i<=cnt;i++)
sum[i]=sum[i-1]+X(chain[i]);
}
int Jump(int x,int K)
{
for(int i=17;i>=0;i--)
if(rk[fa[x][i]]>K) x=fa[x][i];
return fa[x][0];
}
inline int getans(int x,int C)
{
C=C*2-1;
if(C>cnt) return sum[cnt];
if(rk[x]<=C) return sum[C];
int p=Jump(x,C);
return max(sum[C-1]+mxdep[x]-dep[Jump(x,C-1)],sum[C]-mxdep[p]+mxdep[x]);
}
}Sol[2];
int dis[N][2];
void getdis(int x,int pre,int c)
{
for(int i=link[x];i;i=e[i].next)
{
int y=e[i].y;
if(y==pre)continue;
dis[y][c]=dis[x][c]+e[i].v;
getdis(y,x,c);
}
}
int rot[2];
void getroot()
{
getdis(1,0,0);
for(int i=1;i<=n;i++)
if(dis[i][0]>dis[rot[0]][0]) rot[0]=i;
getdis(rot[0],0,1);
for(int i=1;i<=n;i++)
if(dis[i][1]>dis[rot[1]][1]) rot[1]=i;
}
int main()
{
read(n);read(m);
for(int i=1;i<n;i++)
{
int u,v,w;
read(u);read(v);read(w);
add(u,v,w);
add(v,u,w);
}
getroot();
Sol[0].root=rot[0];
Sol[1].root=rot[1];
Sol[0].Build();
Sol[1].Build();
int lst=0;
while(m--)
{
int x,y;
read(x);
read(y);
x=(x+lst-1)%n+1;
y=(y+lst-1)%n+1;
lst=max(Sol[0].getans(x,y),Sol[1].getans(x,y));
printf("%d\n",lst);
}
return 0;
}
首先是经典的0/1分数规划,二分答案后转化为求路径权值是否有 > 0 >0 >0的。
设 f [ x ] [ i ] f[x][i] f[x][i]为从 x x x向下走 y y y条边的最大边权。
那么转移为 f [ x ] [ i ] = m a x ( f [ y ] [ i − 1 ] + w [ x ] [ y ] ) f[x][i]=max(f[y][i-1]+w[x][y]) f[x][i]=max(f[y][i−1]+w[x][y])。
可以通过长链剖分+打标记的形式求出来。
那么答案为
a n s = max i = L R ( max ( f [ x ] [ i ] , f [ y ] [ j ] + f [ z ] [ i − j ] + w [ x ] [ y ] + w [ x ] [ z ] ) ) , i , j ∈ s o n [ x ] ans=\max_{i=L}^R(\max(f[x][i],f[y][j]+f[z][i-j]+w[x][y]+w[x][z])),i,j\in son[x] ans=maxi=LR(max(f[x][i],f[y][j]+f[z][i−j]+w[x][y]+w[x][z])),i,j∈son[x]
这个可以在短儿子向 x x x合并的时候顺便求出来。
但是有一个问题无法解决,就是我们要求一段区间内的最大值。
考虑用线段树维护。
长链剖分后,求出每个点的 d f s dfs dfs序,这样做的好处是一条链上的点在 d f s dfs dfs恰好为一个区间,且某个这条链上的点到链底的距离恰好为这个点在 d f s dfs dfs序上的位置到这个区间的最后一个位置的距离。
也就是说,这个线段树就可以当长链剖分的 d p dp dp数组来使用。
我们可以用 d p [ d f n [ x ] + i ] dp[dfn[x]+i] dp[dfn[x]+i]来存储 f [ x ] [ i ] f[x][i] f[x][i]的值,转移是非常方便的。
同时可以很好地处理一段区间的最大值,那么问题就解决了。
#include
using namespace std;
const int N = 1e6+7;
typedef long long LL;
typedef double db;
template <typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
for(;c>='0'&&c<='9';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
x=(f?-x:x);
}
int n,L,R;
struct edge
{
int y,v,next;
}e[2*N];
int link[N],t=0;
void add(int x,int y,int v)
{
e[++t].y=y;
e[t].v=v;
e[t].next=link[x];
link[x]=t;
}
int len[N],son[N],val[N];
void dfs(int x,int pre)
{
for(int i=link[x];i;i=e[i].next)
{
int y=e[i].y;
if(y==pre)continue;
dfs(y,x);
if(len[y]+1>len[x])
{
len[x]=len[y]+1;
son[x]=y;
val[x]=e[i].v;
}
}
}
const db INF = 1e18;
db dp[N];
inline void clear(int k,int l,int r)
{
dp[k]=-INF;
if(l==r)return;
int mid=(l+r)>>1;
clear(k<<1,l,mid);
clear(k<<1|1,mid+1,r);
}
inline void update(int k,int l,int r,int x,db v)
{
dp[k]=max(dp[k],v);
if(l==r)return;
int mid=(l+r)>>1;
if(x<=mid) update(k<<1,l,mid,x,v);
else update(k<<1|1,mid+1,r,x,v);
}
db query(int k,int l,int r,int L,int R)
{
if(l>R||r<L) return -INF;
if(L<=l&&r<=R) return dp[k];
int mid=(l+r)>>1;
db res=-INF;
if(L<=mid) res=max(res,query(k<<1,l,mid,L,R));
if(R>mid) res=max(res,query(k<<1|1,mid+1,r,L,R));
return res;
}
db ans,mid;
int dfn[N],cnt=0;
db f[N],g[N];
inline db push(int x,db v){return v-g[x];}
void solve(int x,int y,int w)
{
int u=dfn[x],v=dfn[y];
for(int i=1;i<=len[y]+1;i++)
{
db A=query(1,1,n,u+max(1,L-i),u+min(R-i,len[x]));
ans=max(ans,w-mid+f[v+i-1]+g[u]+g[v]+A);
}
for(int i=1;i<=len[y]+1;i++)
{
db A=w-mid+f[v+i-1]+g[v];
db B=g[u]+f[u+i];
if(A>B)
{
f[u+i]=push(u,A);
update(1,1,n,u+i,f[u+i]);
}
}
}
void getans(int x,int pre)
{
if(!dfn[x]) dfn[x]=++cnt;
int u=dfn[x];
f[u]=g[u]=0;
if(son[x])
{
getans(son[x],x);
g[u]=g[u+1]+val[x]-mid;
f[u]=push(u,0);
}
update(1,1,n,u,f[u]);
for(int i=link[x];i;i=e[i].next)
{
int y=e[i].y;
if(y==pre||y==son[x])continue;
getans(y,x);
solve(x,y,e[i].v);
}
if(len[x]>=L)
ans=max(ans,g[u]+query(1,1,n,u+L,u+min(R,len[x])));
}
bool check(db v)
{
clear(1,1,n);
mid=v;
ans=-INF;
getans(1,0);
return ans>=0;
}
int main()
{
read(n);read(L);read(R);
for(int i=1;i<n;i++)
{
int x,y,v;
read(x);read(y);read(v);
add(x,y,v);
add(y,x,v);
}
dfs(1,0);
db l=0,r=1e6;
while(r-l>1e-5)
{
db Mid=(l+r)/2.0;
if(check(Mid)) l=Mid;
else r=Mid;
}
printf("%.3lf\n",l);
return 0;
}
真的很毒瘤啊。
首先因为连通块的交还是连通块,且对于一个连通块而言,点数-边数=1。
所以,我们用点的贡献,减掉边的贡献,就是答案。
设 f [ x ] [ i ] f[x][i] f[x][i]为在 x x x子树内,选择的包含 x x x的连通块的点中距离 x x x最远的距离是 i i i,选择连通块的方案数。
f [ x ] [ i ] = ∏ ( f [ y ] [ i − 1 ] + 1 ) f[x][i]=\prod (f[y][i-1]+1) f[x][i]=∏(f[y][i−1]+1)
维护乘法标记和加法标记,这个可以很轻松地用长链剖分维护。
设 g [ x ] [ i ] g[x][i] g[x][i]为在 x x x的子树为,选择的包含 x x x的连通块的点中距离 x x x最远的距离是 i i i,选择连通块的方案数。
g [ x ] [ i ] = ( g [ f a [ x ] ] [ i − 1 ] + 1 ) [ ∏ y ∈ s o n [ f a [ x ] ] ! = x ( f [ y ] [ i − 2 ] + 1 ) g[x][i]=(g[fa[x]][i-1]+1)[\prod_{y\in son[fa[x]]!=x} (f[y][i-2]+1) g[x][i]=(g[fa[x]][i−1]+1)[∏y∈son[fa[x]]!=x(f[y][i−2]+1)
点的贡献自然是 ( f [ x ] [ L ] × g [ x ] [ L ] ) K (f[x][L]\times g[x][L])^K (f[x][L]×g[x][L])K
边的贡献是 ( f [ x ] [ L − 1 ] ] × g [ x ] [ L − 1 ] K ) (f[x][L-1]]\times g[x][L-1]^K) (f[x][L−1]]×g[x][L−1]K)
但是求 g g g却很麻烦。
我们可以把一个点的 g g g直接继承给长儿子,也算是倒着长链剖分了。
但是后边有点麻烦。
我们把 f a [ x ] fa[x] fa[x]的所有儿子列出来,那么就是一个前缀和一个后缀的乘积。
前缀好解决,因为我们就是正着 d p dp dp的。
但是反面呢?其实也能解决,只要我们第一遍求 f f f时倒着求,并记录下那时的 d p dp dp数组就好了。
细节却有很多。