SPOJ - FTOUR2 Free tour II
problem
给定一棵树,以及 m m m 个拥挤城市编号,选择一条最多包含 k k k 个拥挤城市的简单路径。
每条边有一个有趣度 w w w,可正可负。简单路径的价值定义为包含边的有趣度之和。
求最大价值。 n ≤ 2 e 5 , ∣ w ∣ ≤ 1 e 4 n\le 2e5,|w|\le 1e4 n≤2e5,∣w∣≤1e4。
solution
取点作根,计算过根的符合条件的简单路径价值,如果不过根就转化成各个独立子树的子问题。
选取重心作根,点分治。
问题仍然在于怎么计算过根的符合条件的简单路径价值。
暴力的树形 d p dp dp, d p i , j : i dp_{i,j}:i dpi,j:i 子树内拥挤城市为 j j j 的最大路径价值。
每次枚举 i i i 的儿子,每次做树上背包,之后再合并,时间复杂度是平方级别的。
d p u , x + d p v , y ( x + y ≤ k ) → a n s dp_{u,x}+dp_{v,y}(x+y\le k)\rightarrow ans dpu,x+dpv,y(x+y≤k)→ans
d p u , x = max ( d p u , x , d p v , x ) dp_{u,x}=\max(dp_{u,x},dp_{v,x}) dpu,x=max(dpu,x,dpv,x)
考虑优化。
对于第 i i i 个儿子而言,如果第 i i i 个儿子使用的拥挤城市数量为 j j j,那么前 i − 1 i-1 i−1 个儿子的城市拥挤数量可以使用 1 ∼ k − j 1\sim k-j 1∼k−j。
所以可以“前缀和”优化,这里去前缀最大值
d p u , x : dp_{u,x}: dpu,x: 前 i − 1 i-1 i−1 个儿子城市拥挤数量使用不超过 x x x 的最大价值。
这样就可以线性枚举 j j j 计算贡献。
最后就是在枚举前 i − 1 i-1 i−1 个儿子的拥挤数量问题上。
比如第 p p p 个儿子内部最多可以找到一条路径有 k k k 个拥挤城市,而现在的儿子最多只能有 y ( y < k ) y(y<k) y(y<k) 个。
但是从第 p p p 个儿子开始就必须枚举完使用 k k k 个拥挤城市,才能将前面儿子的信息覆盖完全。
但是这样的时间复杂度就会飞起。
如果交换现在的儿子和第 p p p 个儿子顺序,那么只用枚举完 y y y 个拥挤城市就已经覆盖了前面儿子的所有路径使用情况。
所以将儿子按照内部最多能找到一条路径有 d d d 个拥挤城市, d d d 升序排序。
相当于只将每个点枚举了一次,这就是启发式合并。
code
#include <bits/stdc++.h>
using namespace std;
#define maxn 200005
#define inf 0x7f7f7f7f
vector < pair < int, int > > G[maxn];
int n, m, k, Max, root, N, ans;
bool crowd[maxn], vis[maxn];
int siz[maxn], f[maxn], g[maxn];
struct node { int d, v, w; }MS[maxn];
void dfs( int u, int fa ) {
int maxsiz = 0; siz[u] = 1;
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i].first;
if( vis[v] or v == fa ) continue;
else dfs( v, u ), siz[u] += siz[v];
maxsiz = max( maxsiz, siz[v] );
}
maxsiz = max( maxsiz, N - siz[u] );
if( maxsiz < Max ) Max = maxsiz, root = u;
}
int dfs( int u, int fa, int cnt ) {
if( cnt == k ) return cnt;
int ret = cnt;
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i].first;
if( vis[v] or v == fa ) continue;
ret = max( ret, dfs( v, u, cnt + crowd[v] ) );
}
return ret;
}
void dfs( int u, int fa, int val, int cnt ) {
if( cnt > k ) return;
g[cnt] = max( g[cnt], val );
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i].first, w = G[u][i].second;
if( vis[v] or v == fa ) continue;
dfs( v, u, val + w, cnt + crowd[v] );
}
}
void calc( int u ) {
if( crowd[u] ) k --;
int cnt = 0;
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i].first, w = G[u][i].second;
if( vis[v] ) continue;
MS[++ cnt] = { dfs( v, u, crowd[v] ), v, w };
}//计算出v子树内一条路最多能有多少个拥挤城市
sort( MS + 1, MS + cnt + 1, []( node x, node y ) { return x.d < y.d; } );
//启发式合并
for( int i = 1;i <= cnt;i ++ ) {
int v = MS[i].v, w = MS[i].w;
dfs( v, u, w, crowd[v] ); //计算出v子树内访问x个拥挤城市的最大有趣度
/*
f[j]:前i-1个子树信息总和 访问j个拥挤城市的最大值
g[j]:只针对第i个子树 访问j个拥挤城市的最大值 每次都会在dfn(v,u,w,crowd[v])重新计算
显然 g[j]+f[x](0<=x<=k-j) 都可以对最终答案进行贡献
这里对f[x]进行前缀max 有jx平方的时间变成j线性
实际上x<=MS[i-1].d 要注意这个限制 不然可能会莫须有地更新到不存在的拥挤城市个数 影响f
然后将i子树信息合并到i-1子树信息内 即g->f
转到下一个子树i+1
*/
if( i ^ 1 ) {
for( int j = 1;j <= MS[i - 1].d;j ++ ) f[j] = max( f[j], f[j - 1] );
for( int j = 0;j <= MS[i].d;j ++ ) ans = max( ans, f[min( MS[i - 1].d, k - j )] + g[j] );
}
for( int j = 0;j <= MS[i].d;j ++ ) f[j] = max( f[j], g[j] ), g[j] = 0;
}
for( int i = 0;i <= MS[cnt].d;i ++ ) {
ans = max( ans, f[i] );
f[i] = g[i] = 0;
}
if( crowd[u] ) k ++;
}
void dfs( int u ) {
vis[u] = 1;
calc( u );
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i].first;
if( vis[v] ) continue;
Max = inf, N = siz[v];
dfs( v, u );
dfs( root );
}
}
int main() {
scanf( "%d %d %d", &n, &k, &m );
for( int i = 1, x;i <= m;i ++ ) scanf( "%d", &x ), crowd[x] = 1;
for( int i = 1, u, v, w;i < n;i ++ ) {
scanf( "%d %d %d", &u, &v, &w );
G[u].push_back( { v, w } );
G[v].push_back( { u, w } );
}
Max = inf, N = n;
dfs( 1, 0 );
dfs( root );
printf( "%d\n", ans );
return 0;
}