leetcode.2846. 边权重均等查询【lca+树形dp】

原题链接:2846. 边权重均等查询

题目描述:

现有一棵由 n 个节点组成的无向树,节点按从 0 到 n - 1 编号。给你一个整数 n 和一个长度为 n - 1 的二维整数数组 edges ,其中 edges[i] = [ui, vi, wi] 表示树中存在一条位于节点 ui 和节点 vi 之间、权重为 wi 的边。

另给你一个长度为 m 的二维整数数组 queries ,其中 queries[i] = [ai, bi] 。对于每条查询,请你找出使从 ai 到 bi 路径上每条边的权重相等所需的 最小操作次数 。在一次操作中,你可以选择树上的任意一条边,并将其权重更改为任意值。

注意:

  • 查询之间 相互独立 的,这意味着每条新的查询时,树都会回到 初始状态 。
  • 从 ai 到 bi的路径是一个由 不同 节点组成的序列,从节点 ai 开始,到节点 bi 结束,且序列中相邻的两个节点在树中共享一条边。

返回一个长度为 m 的数组 answer ,其中 answer[i] 是第 i 条查询的答案。

输入输出描述:

示例 1:

leetcode.2846. 边权重均等查询【lca+树形dp】_第1张图片

输入:n = 7, edges = [[0,1,1],[1,2,1],[2,3,1],[3,4,2],[4,5,2],[5,6,2]], queries = [[0,3],[3,6],[2,6],[0,6]]
输出:[0,0,1,3]
解释:第 1 条查询,从节点 0 到节点 3 的路径中的所有边的权重都是 1 。因此,答案为 0 。
第 2 条查询,从节点 3 到节点 6 的路径中的所有边的权重都是 2 。因此,答案为 0 。
第 3 条查询,将边 [2,3] 的权重变更为 2 。在这次操作之后,从节点 2 到节点 6 的路径中的所有边的权重都是 2 。因此,答案为 1 。
第 4 条查询,将边 [0,1]、[1,2]、[2,3] 的权重变更为 2 。在这次操作之后,从节点 0 到节点 6 的路径中的所有边的权重都是 2 。因此,答案为 3 。
对于每条查询 queries[i] ,可以证明 answer[i] 是使从 ai 到 bi 的路径中的所有边的权重相等的最小操作次数。

示例 2:

leetcode.2846. 边权重均等查询【lca+树形dp】_第2张图片

输入:n = 8, edges = [[1,2,6],[1,3,4],[2,4,6],[2,5,3],[3,6,6],[3,0,8],[7,0,2]], queries = [[4,6],[0,4],[6,5],[7,4]]
输出:[1,2,2,3]
解释:第 1 条查询,将边 [1,3] 的权重变更为 6 。在这次操作之后,从节点 4 到节点 6 的路径中的所有边的权重都是 6 。因此,答案为 1 。
第 2 条查询,将边 [0,3]、[3,1] 的权重变更为 6 。在这次操作之后,从节点 0 到节点 4 的路径中的所有边的权重都是 6 。因此,答案为 2 。
第 3 条查询,将边 [1,3]、[5,2] 的权重变更为 6 。在这次操作之后,从节点 6 到节点 5 的路径中的所有边的权重都是 6 。因此,答案为 2 。
第 4 条查询,将边 [0,7]、[0,3]、[1,3] 的权重变更为 6 。在这次操作之后,从节点 7 到节点 4 的路径中的所有边的权重都是 6 。因此,答案为 3 。
对于每条查询 queries[i] ,可以证明 answer[i] 是使从 ai 到 bi 的路径中的所有边的权重相等的最小操作次数。 

提示:

  • 1 <= n <= 1e4
  • edges.length == n - 1
  • edges[i].length == 3
  • 0 <= ui, vi < n
  • 1 <= wi <= 26
  • 生成的输入满足 edges 表示一棵有效的树
  • 1 <= queries.length == m <= 2 * 1e4
  • queries[i].length == 2
  • 0 <= ai, bi < n

解题思路:

首先题目说了每个查询是互不干扰的,所以对于每一个查询只需要单独处理即可,对于每一个查询[a,b],题目要求我们使用最少的修改次数使得a<->b路径上的所有边边权都一样,那么我们只需要先找到这条路径上出现次数最多的边权w,然后将其他边权不等于w的边的权重都修改为w,这样操作就能保证操作次数最少,假设a<->b路径上一共有d条边,那么当前查询最少修改次数就是d-cnt[w],cnt[w]表示边权w出现的次数,那么现在需要做的就是怎么找到出现次数最多的边权出现了多少次,那么最暴力的做法就是将a<->b这条路径上的所有边遍历一遍,这样的时间复杂度为O(n),总共有m次查询,那么这个时候的时间复杂度就是O(n*m),这个题目n=1e4,m=2e4,那么时间就是2e8了,这个时间复杂度就很高了,大概率是过不了了,我们考虑怎么进行优化,首先这是一棵树,每次查询一般是可以优化到log(n)的,这个时候常用的优化方式就应该想到和最近公共祖先(lca)有关了,由于边的权重只有26种,我们可以暴力枚举26种边,看哪种边出现次数最多,那么a<->b之间某种边i出现的次数就是f[a][i]+f[b][i]-f[lca(a,b)][i]*2,通过枚举26种边就可以知道出现次数最多的边的出现次数为多少,lca的时间复杂度为log(n),每次枚举26种边,这样每次查询就优化到了26+log(n),n=1e4,那么log(n)大概就是14,14+26=40,总的时间大概是m*40=2e4*40,时间粗略估计大概就是8e5,这个时间复杂度是可以过的,下面时间复杂度分析处分析的会更仔细。

时间复杂度:bfs预处理时间复杂度为O(m+n*14),dp预处理时间复杂度为O(m+n*26),然后查询的时间复杂度为O(m*(log(n)+26)),综合时间大概为40*m+n*40,时间复杂度为O(40*(n+m)),时间大概是40*(1e4+2e4),大概就是40*3e4,花费时间大概就是1.2e6,这个时间是肯定可以过的。

空间复杂度:空间大概是n*50=1e4*50*4=2e6/1e6=2M,单个测试数据这个空间需求非常低,所以空间是肯定足够的,空间复杂度为O(n),但是n前面的常数为50左右。

cpp代码如下:

const int N=1e4+10,M=N*2;
int f[N][26],fa[N][15];
int h[N],w[M],e[M],ne[M],idx;
int q[N],depth[N];
class Solution {
    void add(int a,int b,int c)
    {
        e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
    }
    void bfs(int root)
    {
        memset(depth,0x3f,sizeof depth);
        int hh=0,tt=0;
        depth[0]=0,depth[root]=1;
        q[0]=root;
        while(hh<=tt)
        {
            int t=q[hh++];
            for(int i=h[t];i!=-1;i=ne[i])
            {
                int j=e[i];
                if(depth[j]>depth[t]+1)
                {
                    depth[j]=depth[t]+1;
                    fa[j][0]=t;
                    q[++tt]=j;
                    for(int k=1;k<=14;k++)
                        fa[j][k]=fa[fa[j][k-1]][k-1];
                }
            }
        }
    }

    void dp(int u,int father)
    {
        for(int i=h[u];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(j==father)continue;
            for(int k=0;k<26;k++)
                f[j][k]=f[u][k];
            f[j][w[i]]++;
            dp(j,u);
        }

    }

    int lca(int a,int b)
    {
        if(depth[a]=0;k--)
            if(depth[fa[a][k]]>=depth[b])
                a=fa[a][k];
        if(a==b)return a;
        for(int k=14;k>=0;k--)
            if(fa[a][k]!=fa[b][k])
            {
                a=fa[a][k];
                b=fa[b][k];
            }
        return fa[a][0];
    }
public:
    vector minOperationsQueries(int n, vector>& edges, vector>& queries) {
        //初始化
        for(int i=0;ians;
        //处理每一次询问
        for(auto que:queries){
            int u=que[0]+1,v=que[1]+1;  //点的编号变化了,这里跟着变化
            int p=lca(u,v);
            int d=depth[u]+depth[v]-depth[p]*2;  //u,v之间总的边数
            int res=d;
            //枚举26种边
            /*
                d表示u,v之间总的边数,f[u][k]+f[v][k]-f[p][k]*2)表示
                u,v之间边权为k的边出现的次数.
                那么d-f[u][k]+f[v][k]-f[p][k]*2)表示需要修改的边数,
                用来更新答案
            */
            for(int k=0;k<26;k++)
                res=min(res,d-(f[u][k]+f[v][k]-f[p][k]*2));
            ans.push_back(res);
        }
        //输出答案
        return ans;
    }
};

你可能感兴趣的:(图论,算法,leetcode)