原题链接: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:
输入: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;
}
};