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

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

题目描述:

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

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

注意:

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

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

输入输出描述:

示例 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:

复制代码
输入: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代码如下:

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]<depth[b])swap(a,b);
        for(int k=14;k>=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<int> minOperationsQueries(int n, vector<vector<int>>& edges, vector<vector<int>>& queries) {
        //初始化
        for(int i=0;i<n+5;i++)h[i]=-1;
        idx=0;
        //建图
        for(int i=0;i<n-1;i++)
        {
            int u=edges[i][0],v=edges[i][1],w=edges[i][2];
            //将点的编号由0~n-1变为1~n,将边的权重由1~26变为0~25
            u++,v++,w--;
            add(u,v,w),add(v,u,w);
        }

        //题目是无向树,没有规定根节点,我们随便选一个根节点就行,这里我选的1号结点为根节点
        //bfs预处理lca中的depth数组和f数组
        bfs(1);

        //dp预处理
        //f[i][j]表示根节点到i号结点上边权为j的边出现的次数
        dp(1,-1);

        vector<int>ans;
        //处理每一次询问
        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;
    }
};
相关推荐
手握风云-13 分钟前
零基础Java第十六期:抽象类接口(二)
数据结构·算法
笨小古1 小时前
路径规划——RRT-Connect算法
算法·路径规划·导航
<但凡.1 小时前
编程之路,从0开始:知识补充篇
c语言·数据结构·算法
f狐0狸x2 小时前
【数据结构副本篇】顺序表 链表OJ
c语言·数据结构·算法·链表
paopaokaka_luck2 小时前
基于Spring Boot+Vue的多媒体素材管理系统的设计与实现
java·数据库·vue.js·spring boot·后端·算法
视觉小萌新2 小时前
VScode+opencv——关于opencv多张图片拼接成一张图片的算法
vscode·opencv·算法
2的n次方_2 小时前
二维费用背包问题
java·算法·动态规划
simple_ssn3 小时前
【C语言刷力扣】1502.判断能否形成等差数列
c语言·算法·leetcode
寂静山林3 小时前
UVa 11855 Buzzwords
算法
Curry_Math3 小时前
LeetCode 热题100之技巧关卡
算法·leetcode