perface
先扯一点没用的话,可以跳过。
考试的时候老师没有给来源,下意识以为是出的原题???
然后发的 solution 因为在机房里打打闹闹,电脑重启,极域被关掉了,没收到就以为没有???
“幸好”之前存的 std 没有被还原掉,于是就直接盯着 std 硬看,猜思路,猜状态,疯狂画图
最后实在没办法,请教机房的大佬,然后大佬直接对着 luogu 上的题面给我讲???
woc ,这个不是原题???(白盯了这么久)
于是决定写篇题解纪念一下。
题解的大致逻辑是考场思路(当然我考场并没有做出来)
Statement
Solve
考试的时候想到了的性质 :
- 起点一定会用一个,用完肯定走
显然,如果起点不用,那么不如直接在使用第一个磁铁的地方开始
因为在另外一个地方后使用磁铁贡献是 \(st^{\prime}\) 周围 \(f\) 的和减去红点的 \(f\)
而直接在 \(st^{\prime}\) 使用磁铁则不需要减去红色部分的值
显然,磁铁用完后,贡献不会增加。
这个性质可以引导我们枚举起点终点 就这 ,考试的时候也并不是很会用
-
假如一个点 \(u\) 被放置了磁铁,那么它的贡献可以表示为 \(\sum_{(u\to v)} f[v] \ \ \ \ -f[pre]\)
其中 \(pre\) 是点 \(u\) 的上一个点。
能不能再挖掘出什么有用的性质呢?布吉岛,先打暴力再说
20pts
暴力枚举嘛,终点起点,放或不放,\(O(n^22^n)\)
40pts
考虑树上 \(dp\)
设 \(dp[i][j][0/1]\) 表示以 \(rt\) 为根的树,点 \(i\) 到子树内某一点,用了 \(j\) 个磁铁,点 \(i\) 用不用磁铁的最大贡献,那么
其中 \(sum[u]\) 表示的是以 \(rt\) 为根的树中,\(\sum f[son_u]\)
第一个没啥好说的
第二个主要是看 \(sum[u]\) 什么意思,因为是从 \(u\) 到子树内,所以选 \(u\) 的时候,\(f[fath]+\sum f[son]\) 都会被吸引,但是因为我们是从 \(fath\) 走过来的,所以 \(f[fath]\) 没有贡献,其他都有贡献,刚刚好就是 \(sum[u]\)
这样一遍是 \(O(nv)\) 的,发现 \(rt\) 的意义和起点相同,所以还要枚举起点 \(rt\) ,总共就是 \(O(n^2v)\)
思考发现,好像最后半维 \([0/1]\) 是没必要的,上面两个式子可以合并成:
这可以小小优化常数,还可以加入优化:磁铁为 \(0\) 则离开
70pts
即固定 \(rt=1\) (但是怎么判断呢)
100pts
考虑对 \(dp\) 进行优化
发现整个过程最愚蠢的地方就是枚举 \(rt\) ,所以考虑换根???
@#¥%……&* 乱推勾 \(8\) 了一下式子,发现这个 \(\max\) 不会处理 \(50pts\) 走人。
好的,现在赛后来看,大体有两种思路:
Way1
延续换根 \(dp\) 的思想,考虑如何强行处理 \(\max\)
假设我们已经求到了 \(rt=fath\) 意义下的 \(dp[i][j]\)
考虑最后得到的 \(rt=u\) 意义下的路径会是怎么样:
会是从 \(u\to v_1\) 或者 \(u\to v_2\)
这引导我们把 \(fath\) 的所有子树拍进一个 \(vector\) 里面
然后维护 \(dp[v][j](v\in son_{fath})\) 的前缀 \(pre\),后缀 \(suf\) 最大值
那么图中红线贡献 \(=pre+(sum[fath]+f[father]-f[u])+(sum[u]+f[fath])\)
那么图中蓝线贡献 \(=suf+(sum[fath]+f[father]-f[u])+(sum[u]+f[fath])\)
上式第一组括号内是 \(fath\) 的贡献,第二组是 \(u\to fath\) 的贡献
当然,上面的式子是在 \(fath\) 和 \(u\) 都放磁铁的情况。
接下来,我觉得就可以直接通过代码理解了。
Code1
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5+5;
const int M = 1e2+5;
struct Edge{
int nex,to;
}edge[N<<1];
int head[N],dp[N][M],sum[N],f[N];
int n,m,elen,ans;
void addedge(int u,int v){
edge[++elen]={head[u],v},head[u]=elen;
edge[++elen]={head[v],u},head[v]=elen;
}
void dfs(int u,int fath){
for(int e=head[u],v;e;e=edge[e].nex)
if((v=edge[e].to)!=fath)
sum[u]+=f[v],dfs(v,u);
for(int e=head[u],v;e;e=edge[e].nex)
if((v=edge[e].to)!=fath)
for(int i=1;i<=m;++i)
dp[u][i]=max(dp[u][i],max(dp[v][i],dp[v][i-1]+sum[u]));//在 rt=1 意义下的 dp 求解
}
void dfs2(int u,int fath){
vector<int>son,pre[M];int suf[M];
memset(suf,0,sizeof(suf));
//son 记录所有与 u 相连的点(包括 fath)
//pre[i] 记录使用 i 个磁铁的前缀最大值
//suf[i] 记录使用 i 个磁铁的后缀最大值
for(int e=head[u],v;e;e=edge[e].nex){
son.push_back(v=edge[e].to);
for(int i=1;i<=m;++i)
ans=max(ans,dp[u][i]=max(dp[u][i],max(dp[v][i],dp[v][i-1]+sum[u]+f[fath]))),//在 rt=u 的意义下(起点为u)的 dp 数组,注意在起点时,周围所有点都可以贡献
dp[u][i]=0;//清零,为儿子的求解做准备
}
for(int i=0,tmp=0;i<=m;++i,tmp=0)
for(auto v:son)
tmp=max(tmp,dp[v][i]),
pre[i].push_back(tmp);//pre[i][j] 表示前 j 个儿子,使用 i 个磁铁最大值
for(int i=(int)son.size()-1;i;--i){
int v=son[i];
for(int j=m;j;--j)//倒序枚举,方便得到 suf
dp[u][j]=max(dp[u][j],max(max(suf[j],suf[j-1]+sum[u]+f[fath]-f[v]),
max(pre[j][i-1],pre[j-1][i-1]+sum[u]+f[fath]-f[v]))),//放/不放磁铁
suf[j]=max(suf[j],dp[v][j]);
//此时, dp[u][j] 的意义变成了从 u 走到子树内 v^{\prime} (v^{\prime}\neq v) 的最大值
if(v!=fath)dfs2(v,u);//////////////////
for(int j=1;j<=m;++j)dp[u][j]=0;
}
if(son.size()){//第一个儿子,没有办法用 pre 更新
int v=son[0];
for(int j=1;j<=m;++j)
dp[u][j]=max(dp[u][j],max(suf[j],suf[j-1]+sum[u]+f[fath]-f[v]));
if(v!=fath)dfs2(v,u);///////////////
}
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;++i)scanf("%lld",&f[i]);
for(int i=1,u,v;i<n;++i)
scanf("%lld%lld",&u,&v),addedge(u,v);
dfs(1,0);
dfs2(1,0);
printf("%lld\n",ans);
return 0;
}
Way2
我们可以转化问题:一棵树,点有点权,求在一个计算法则下的直径
(所谓计算法则就是磁铁的使用)
考虑常规树的直径是怎么做的:设 \(d1[u],d2[u]\) 分别表示从 \(u\) 走到 \(u\) 子树内的最长路和次长路
那么,\(ans=\max\{d1[u]+d2[u]\}\)
尝试套到本题,特殊的是,从 上往下走 和 从下往上 走同一条路径的贡献并不相同
那么,我们设 \(up[i][j][0/1]\) 表示从 \(i\) 的子树内走到 \(i\) ,使用 \(j\) 块磁铁, \(i\) 放不放磁铁的最大贡献
同理,设 \(down[i][j][0/1]\)
容易写出状态转移方程:
初态:
up[u][0][1] = down[u][0][1] = -inf;
up[u][1][1] = max(up[u][1][1], sum[u] + f[fath]);
因为显然。
答案可以通过前缀最大值更新
Code2
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
const int inf = 1e18;
struct Edge {
int nex, to;
} edge[N << 1];
int head[N], f[N], sum[N];
int up[N][105][2], down[N][105][2];
int n, V, elen, ans;
void addedge(int u, int v)
{
edge[++elen] = { head[u], v }, head[u] = elen;
edge[++elen] = { head[v], u }, head[v] = elen;
}
void DP(int u, int fath)
{
for (int e = head[u], v; e; e = edge[e].nex) {
if ((v = edge[e].to) == fath)
continue;
DP(v, u);
sum[u] += f[v];
}
up[u][0][1] = down[u][0][1] = -inf;
up[u][1][1] = max(up[u][1][1], sum[u] + f[fath]);
for (int e = head[u], v; e; e = edge[e].nex) {
if ((v = edge[e].to) == fath)
continue;
int mx1 = 0, mx2 = 0;
for (int i = V; ~i; --i) {
mx1 = max(mx1, max(up[u][V - i][0], up[u][V - i][1]));
mx2 = max(mx2, max(down[u][V - i][0], down[u][V - i][1] + f[fath] - f[v]));
ans = max(ans, max(down[v][i][0], down[v][i][1]) + mx1);
ans = max(ans, max(up[v][i][0], up[v][i][1]) + mx2);
}
for (int i = 1; i <= V; ++i)
up[u][i][0] = max(up[u][i][0], max(up[v][i][0], up[v][i][1])),
up[u][i][1] = max(up[u][i][1], max(up[v][i - 1][0], up[v][i - 1][1]) + sum[u] - f[v] + f[fath]),
down[u][i][0] = max(down[u][i][0], max(down[v][i][0], down[v][i][1])),
down[u][i][1] = max(down[u][i][1], max(down[v][i - 1][0], down[v][i - 1][1]) + sum[u]);
}
}
signed main()
{
scanf("%lld%lld", &n, &V);
for (int i = 1; i <= n; ++i)
scanf("%lld", &f[i]);
for (int i = 1, u, v; i < n; ++i)
scanf("%lld%lld", &u, &v), addedge(u, v);
DP(1, 0);
printf("%lld\n", ans);
return 0;
}