SPOJ - FTOUR2 Free tour II
problem
给定一棵树,以及 mmm 个拥挤城市编号,选择一条最多包含 kkk 个拥挤城市的简单路径。
每条边有一个有趣度 www,可正可负。简单路径的价值定义为包含边的有趣度之和。
求最大价值。n≤2e5,∣w∣≤1e4n\le 2e5,|w|\le 1e4n≤2e5,∣w∣≤1e4。
solution
取点作根,计算过根的符合条件的简单路径价值,如果不过根就转化成各个独立子树的子问题。
选取重心作根,点分治。
问题仍然在于怎么计算过根的符合条件的简单路径价值。
暴力的树形 dpdpdp,dpi,j:idp_{i,j}:idpi,j:i 子树内拥挤城市为 jjj 的最大路径价值。
每次枚举 iii 的儿子,每次做树上背包,之后再合并,时间复杂度是平方级别的。
dpu,x+dpv,y(x+y≤k)→ansdp_{u,x}+dp_{v,y}(x+y\le k)\rightarrow ansdpu,x+dpv,y(x+y≤k)→ans
dpu,x=max(dpu,x,dpv,x)dp_{u,x}=\max(dp_{u,x},dp_{v,x})dpu,x=max(dpu,x,dpv,x)
考虑优化。
对于第 iii 个儿子而言,如果第 iii 个儿子使用的拥挤城市数量为 jjj,那么前 i−1i-1i−1 个儿子的城市拥挤数量可以使用 1∼k−j1\sim k-j1∼k−j。
所以可以“前缀和”优化,这里去前缀最大值
dpu,x:dp_{u,x}:dpu,x: 前 i−1i-1i−1 个儿子城市拥挤数量使用不超过 xxx 的最大价值。
这样就可以线性枚举 jjj 计算贡献。
最后就是在枚举前 i−1i-1i−1 个儿子的拥挤数量问题上。
比如第 ppp 个儿子内部最多可以找到一条路径有 kkk 个拥挤城市,而现在的儿子最多只能有 y(y<k)y(y<k)y(y<k) 个。
但是从第 ppp 个儿子开始就必须枚举完使用 kkk 个拥挤城市,才能将前面儿子的信息覆盖完全。
但是这样的时间复杂度就会飞起。
如果交换现在的儿子和第 ppp 个儿子顺序,那么只用枚举完 yyy 个拥挤城市就已经覆盖了前面儿子的所有路径使用情况。
所以将儿子按照内部最多能找到一条路径有 ddd 个拥挤城市,ddd 升序排序。
相当于只将每个点枚举了一次,这就是启发式合并。
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;
}