2020牛客暑假多校第十场补题

比赛链接:link


   

C 边权转点权

   题目大意是一棵树给定边权,都是非负数,树上的每条路径可以使得经过的边的边权减 1,问最少需要几次路径? 其中还有 q 次修改,每次修改一条边权,每次修改完之后输出更新后的答案。
   经过分析可以发现,若记 w ( u , v ) w(u, v) w(u,v) u u u, v v v 间的边权,那么就存在 w ( u , v ) w(u, v) w(u,v) 条路径通过 u , v u, v u,v。可以举几个例子看看,比如 u u u 连有三个点 v 1 , v 2 , v 3 v_1, v_2, v_3 v1,v2,v3
   ①若边权分别为 10,5,2, 可以用 7 条 u v 1 uv_1 uv1 的路径可以与所有 u v 2 , u v 3 uv_2, uv_3 uv2,uv3 的路径相连接,那么 u u u 至少是 3 条路径的端点;
   ②若边权分别是5,4,3,那么 3条 u v 1 uv_1 uv1 的路径可以与 3条 u v 2 uv_2 uv2 路径连接, 2条 u v 1 uv_1 uv1 的路径可以与 2条 u v 3 uv_3 uv3 路径连接,1条 u v 2 uv_2 uv2 的路径可以与 1条 u v 3 uv_3 uv3 路径连接, u u u 端点数可以为 0 0 0
   ②若边权分别是5,4,2,由于是奇数,那么不管怎么连接, u u u 端点数至少为 1 1 1
   通过上面的举例我们可以得到,若是一个点的边权和为 d, 最大边权为 mx,大于其他边权和 d - mx, 那么这个点的路径端点数为 mx - (d - mx) = 2*mx - d;若最大边权小于等于其他边权,若 d 为偶数,则可以互相连接,端点数为 0, 否则奇数的话端点数为1。
   于是我们需要维护每个点的边权和,这个用一个数组就可以,还需要维护最大边权,这个可以用 multiset 来做到;计算完所有点的端点数之后,除以 2 为我们所需要答案。每次更新我们只需要相应的更改数组和 multiset, 复杂度为 O ( l o g n ) O(logn) O(logn)

#include <bits/stdc++.h>
#define pb push_back

using namespace std;

typedef long long ll;
typedef pair<int, int> P;
const int maxn = 1e5 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;

int x[maxn], y[maxn], val[maxn], n, q, p, w;
ll d[maxn];
multiset<int> s[maxn];

int cal(int x)               //计算每个点的路径端点数
{
    ll mx = *s[x].rbegin();
    if(mx * 2 <= d[x])  return d[x] & 1;
    return 2 * mx - d[x];
}

int main()
{
    scanf("%d %d", &n, &q);
    for(int i = 1; i < n; i++)
    {
        scanf("%d %d %d", &x[i], &y[i], &val[i]);
        d[x[i]] += val[i];  d[y[i]] += val[i];
        s[x[i]].insert(val[i]);  s[y[i]].insert(val[i]);
    }

    ll ans = 0;
    for(int i = 1; i <= n; i++)  ans += cal(i);
    printf("%lld\n", ans / 2);
    while(q--)
    {
        scanf("%d %d", &p, &w);
        ans -= cal(x[p]) + cal(y[p]);
        s[x[p]].erase(s[x[p]].find(val[p]));  s[y[p]].erase(s[y[p]].find(val[p]));
        s[x[p]].insert(w);  s[y[p]].insert(w);
        d[x[p]] += w - val[p];  d[y[p]] += w - val[p];
        val[p] = w;
        ans += cal(x[p]) + cal(y[p]);
        printf("%lld\n", ans / 2);
    }
}

   

J 树型dp & 二分图权值匹配

   题目大意是给定两棵形状一样的树,每个点都有不同的值,问第一棵树最少改变几个结点的值可以和第二棵完全相同?
   我一开始想的是递归去做,首先判断某两个点的子树是否相同,然后对其相同的儿子分别去试试(比如儿子 1 , 2 , 3 1, 2, 3 1,2,3形状相同,那么可以有 3 ! 3! 3种试法),这样搞是阶乘复杂度,炸内存了。。
   其实这个匹配的过程可以用二分图来完成,降到多项式复杂度。首先 d p [ x ] [ y ] dp[x][y] dp[x][y] 表示第一二棵树分别以 x x x, y y y 为结点的子树要相同的最少花费,若是形状都不一样就让值为 I N F INF INF,这个过程我们可以先进行树型dp,求出 d p [ a ] [ b ] dp[a][b] dp[a][b] ( a a a b b b 分别为 x x x, y y y 的儿子),然后进行二分图的权值匹配,得到 d p [ x ] [ y ] dp[x][y] dp[x][y],这个思想比较巧妙。
   然后顺便复习了一下 km 算法的板子,才知道 km 算法是完备匹配…由于km 算法的数组 val 是全局变量,要首先将所有的 d p [ a ] [ b ] dp[a][b] dp[a][b]算完,再去更新 km 算法的数组,切记
   

#include <bits/stdc++.h>
#define pb push_back

using namespace std;

typedef long long ll;
typedef pair<int, int> P;
const int maxn = 510;
const int INF = 1e6;
const ll mod = 998244353;

int val[maxn][maxn], vis_A[maxn], vis_B[maxn], match[maxn];     //vis来记录已被匹配的,match记录B方匹配到了A方的哪个
int ex_A[maxn], ex_B[maxn], slack[maxn], m, n;                        //记录A方和B方的期望, A方有m个,B方有n个
//slack 任意一个参与匹配A方能换到任意一个这轮没有被选择过的B方所需要降低的最小值
//这是二分图的最优匹配(首先是A集合的完备匹配,然后保证权值最大)
//所以一定保证 m <= n, 否则会陷入死循环,若是A集合点多的话可以把B集合补充到和A一样多,设置-INF的边
bool dfs(int x)
{
    vis_A[x] = 1;
    for(int i = 1; i <= n; i++)
    {
        if(!vis_B[i])                             //每一轮匹配,B方每一个点只匹配一次
        {
            int gap = ex_A[x] + ex_B[i] - val[x][i];
            if(gap == 0)                         //如果符合要求
            {
                vis_B[i] = 1;
                if(!match[i] || dfs(match[i]))   //如果v尚未匹配或者匹配了可以被挪走
                {
                    match[i] = x;
                    return true;
                }
            }
            else slack[i] = min(slack[i], gap);
        }
    }
    return false;
}

int km()
{
    memset(match, 0, sizeof(match));            //match为0表示还没有匹配
    fill(ex_B + 1, ex_B + 1 + n, 0);             //B方一开始期望初始化为0
    for(int i = 1; i <= m; i++)                 //A方期望取最大值
    {
        ex_A[i] = val[i][1];
        for(int j = 2; j <= n; j++)
            ex_A[i] = max(ex_A[i], val[i][j]);
    }
    for(int i = 1; i <= m; i++)               //尝试解决A方的每一个节点
    {
        memset(slack + 1, INF, sizeof(slack[0]) * n);
        for(;;)
        {
            memset(vis_A + 1, 0, sizeof(vis_A[0]) * m);     //记录AB双方有无被匹配过
            memset(vis_B + 1, 0, sizeof(vis_B[0]) * n);
            if(dfs(i))  break;
            int d = INF;
            for(int j = 1; j <= n; j++)    if(!vis_B[j]) d = min(d, slack[j]);
            //if(d == INF)  break;                        //找不到完全匹配
            for(int j = 1; j <= m; j++) if(vis_A[j]) ex_A[j] -= d;
            for(int j = 1; j <= n; j++)
            {
                if(vis_B[j]) ex_B[j] += d;
                else slack[j] -= d;
            }
        }
    }
    int ans = 0;
    for(int i = 1; i <= n; i++)
    {
        if(match[i])              // 可以加 && val[match[i]][i] > -INF 去除一些匹配
            ans += val[match[i]][i];
    }
    return ans;
}

vector<int> G1[maxn], G2[maxn];
int dp[maxn][maxn], rt1, rt2;

void solve(int x, int y)
{
    if(G1[x].size() != G2[y].size())
    {
        dp[x][y] = INF;
        return;
    }

    if(x != y)  dp[x][y]++;
    if(G1[x].size() == 0)  return;

    for(unsigned int i = 0; i < G1[x].size(); i++)
        for(unsigned int j = 0; j < G2[y].size(); j++)
            solve(G1[x][i], G2[y][j]);                //切记先让子树完成之后再更新val

    for(unsigned int i = 0; i < G1[x].size(); i++)
        for(unsigned int j = 0; j < G2[y].size(); j++)
            val[i+1][j+1] = -dp[G1[x][i]][G2[y][j]];       //因为是求最小匹配,所以置为负数,不存在的边置为-INF
    m = n = G1[x].size();
    int tmp = -km();
    if(tmp >= INF)  dp[x][y] = INF;
    else dp[x][y] += tmp;
}

int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
    {
        int tmp;
        scanf("%d", &tmp);
        if(tmp == 0)  rt1 = i;
        else  G1[tmp].pb(i);
    }
    for(int i = 1; i <= n; i++)
    {
        int tmp;
        scanf("%d", &tmp);
        if(tmp == 0)  rt2 = i;
        else  G2[tmp].pb(i);
    }
    solve(rt1, rt2);
    printf("%d\n", dp[rt1][rt2]);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值