洛谷P3177 [HAOI2015] 树上染色 题解

洛谷P3177 [HAOI2015] 树上染色 题解

题目链接:P3177 [HAOI2015] 树上染色

题意

有一棵点数为 n n n 的树,树边有边权。给你一个在 0 ∼ n 0 \sim n 0n 之内的正整数 m m m ,你要在这棵树中选择 m m m 个点,将其染成黑色,并将其他 的 n − m n-m nm 个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的受益。问受益最大值是多少。

upd.20220531 由于本人对树上背包掌握不熟,导致复杂度写假了

其实是题解区普遍写错罢了。

不过原文只是没用刷表法

于是在原文基础上做了修改,不影响观感,请放心食用(逃


显然树上背包

d p [ u ] [ j ] dp[u][j] dp[u][j] 表示 u u u 所在子树染了 j j j 个黑点的最大价值

容易推出一个大概的方程
d p [ u ] [ j ] = max ⁡ ( d p [ u ] [ j ] , d p [ u ] [ j − k ] + d p [ v ] [ k ] + val ) dp[u][j]=\max(dp[u][j],dp[u][j-k]+dp[v][k]+\text{val}) dp[u][j]=max(dp[u][j],dp[u][jk]+dp[v][k]+val)
注:下文会提到这个转移方程是有点问题的

黑点两两距离+白点两两距离

直接去算就是 O ( n 2 ) O(n^2) O(n2) 的了

考虑更好的计算方法

一条路径会包括若干条边

因为是树所以有很多的边会被重复走过

考虑将距离计算转化为边重复经过次数

根据乘法原理,可知边 ( u , v ) (u,v) (u,v) u u u 为父结点)的贡献为
val = w ( u , v ) × ( k ( m − k ) + ( sz [ v ] − k ) ( n − m − sz [ v ] + k ) ) \text{val}=w(u,v)\times(k(m-k)+(\text{sz}[v]-k)(n-m-\text{sz}[v]+k)) val=w(u,v)×(k(mk)+(sz[v]k)(nmsz[v]+k))
upd.20220531 然后填表法写出来就是这样的(原文代码)

for(int i=head[u]; i; i=e[i].next)
{
    int v=e[i].v;
    if(v==f)continue;
    dfs(v,u);
    sz[u]+=sz[v];
    for(int j=min(sz[u],m); j>=0; j--)
    {
        if(dp[u][j]>=0) // k正着枚举的时候这个就不用了
            dp[u][j]+=dp[v][0]+sz[v]*(n-m-sz[v])*e[i].w;
        for(int k=min(sz[v],j); k>0; k--) // 这里正着枚举也可以
        {
            if(dp[u][j-k]<0)continue;
            int val=(k*(m-k)+(sz[v]-k)*(n-m-sz[v]+k))*e[i].w;
            dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]+val);
        }
    }
}

不难发现这个其实复杂度是可以被卡到 O ( n m 2 ) O(nm^2) O(nm2)

所以考虑刷表法,即
d p [ u ] [ j + k ] = max ⁡ ( d p [ u ] [ j + k ] , tmp [ j ] + d p [ v ] [ k ] + val ) dp[u][j+k]=\max(dp[u][j+k],\text{tmp}[j]+dp[v][k]+\text{val}) dp[u][j+k]=max(dp[u][j+k],tmp[j]+dp[v][k]+val)
这里的 tmp [ j ] \text{tmp}[j] tmp[j] 是上一轮的 d p [ u ] [ j ] dp[u][j] dp[u][j] ,刷表的过程中会被刷坏,所以要先存一下

这样时间复杂度才是严格的 O ( n m ) O(nm) O(nm)

代码:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
#define N (int)(2e3+15)

int n,m;
struct Edge
{
    int u,v,w,next;
}e[N<<1];
int pos=1,head[N],sz[N],tmp[N],dp[N][N];
void addEdge(int u,int v,int w)
{
    e[++pos]={u,v,w,head[u]};
    head[u]=pos;
}
void dfs(int u,int f)
{
    sz[u]=1;
    dp[u][0]=dp[u][1]=0;
    for(int i=head[u]; i; i=e[i].next)
    {
        int v=e[i].v;
        if(v==f)continue;
        dfs(v,u);
        for(int j=0; j<=min(sz[u],m); j++)
            tmp[j]=dp[u][j];
        for(int j=0; j<=min(sz[u],m); j++)
            for(int k=0; k<=sz[v]&&j+k<=m; k++)
            {
                if(tmp[j]<0)continue;
                int val=(k*(m-k)+(sz[v]-k)*(n-m-sz[v]+k))*e[i].w;
                dp[u][j+k]=max(dp[u][j+k],tmp[j]+dp[v][k]+val);
            }
        sz[u]+=sz[v];
    }
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    // freopen("check.in","r",stdin);
    // freopen("check.out","w",stdout);
    cin >> n >> m;
    for(int i=1,u,v,w; i<n; i++)
    {
        cin >> u >> v >> w;
        addEdge(u,v,w);addEdge(v,u,w);
    }
    memset(dp,0xc0,sizeof(dp));
    dfs(1,1);
    cout << dp[1][m] << endl;
    return 0;
}

这一段可以跳过,只是详细解释了link的东西,

而且ta的代码复杂度也是假的,因为都是填表法

注意到有些人 k k k 倒序枚举,要先转移 k = 0 k=0 k=0 的情况,这里解释一下

关于为什么要先将 k = 0 k=0 k=0 的转移

观察方程,因为如果直接倒序枚举,最后一次 k = 0 k=0 k=0 的枚举

会出现 d p [ u ] [ j ] = max ⁡ ( d p [ u ] [ j ] , d p [ u ] [ j ] + val ) dp[u][j]=\max(dp[u][j],dp[u][j]+\text{val}) dp[u][j]=max(dp[u][j],dp[u][j]+val) 的情况

显然此时的 d p [ u ] [ j ] dp[u][j] dp[u][j] 已经被更新过了

因此会导致答案有误(偏大)

除了 k = 0 k=0 k=0 的特殊情况,其他时候 k k k 随便啥顺序枚举都是可以的

转载请说明出处

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这道题目还可以使用状数组或线段来实现,时间复杂度也为 $\mathcal{O}(n\log n)$。这里给出使用状数组的实现代码。 解题思路: 1. 读入数据; 2. 将原数列离散化,得到一个新的数列 b; 3. 从右往左依次将 b 数列中的元素插入到状数组中,并计算逆序对数; 4. 输出逆序对数。 代码实现: ```c++ #include <cstdio> #include <cstdlib> #include <algorithm> const int MAXN = 500005; struct Node { int val, id; bool operator<(const Node& other) const { return val < other.val; } } nodes[MAXN]; int n, a[MAXN], b[MAXN], c[MAXN]; long long ans; inline int lowbit(int x) { return x & (-x); } void update(int x, int val) { for (int i = x; i <= n; i += lowbit(i)) { c[i] += val; } } int query(int x) { int res = 0; for (int i = x; i > 0; i -= lowbit(i)) { res += c[i]; } return res; } int main() { scanf("%d", &n); for (int i = 1; i <= n; ++i) { scanf("%d", &a[i]); nodes[i] = {a[i], i}; } std::sort(nodes + 1, nodes + n + 1); int cnt = 0; for (int i = 1; i <= n; ++i) { if (i == 1 || nodes[i].val != nodes[i - 1].val) { ++cnt; } b[nodes[i].id] = cnt; } for (int i = n; i >= 1; --i) { ans += query(b[i] - 1); update(b[i], 1); } printf("%lld\n", ans); return 0; } ``` 注意事项: - 在对原数列进行离散化时,需要记录每个元素在原数列中的位置,便于后面计算逆序对数; - 设状数组的大小为 $n$,则状数组中的下标从 $1$ 到 $n$,而不是从 $0$ 到 $n-1$; - 在计算逆序对数时,需要查询离散化后的数列中比当前元素小的元素个数,即查询 $b_i-1$ 位置上的值; - 在插入元素时,需要将离散化后的数列的元素从右往左依次插入状数组中,而不是从左往右。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值