Leetcode 每日一题:Evaluate Division

news/2024/9/19 1:58:45 标签: leetcode, 算法, 职场和发展

写在前面

今天依旧是一道来自图论的题目,而今天这道题目的难度也是相对于前面分享过的题目中难度最大的一种。题主在第一次做这道题的时候根本没有反应过来这道题目可以转化为 图 来解决。而这道题目将一个 二维数组的乘除 运算转化为 图论 的过程需要一定的数学思维思考,今天就让我们一起来看看这道题怎么个事儿吧~~

题目介绍:

题目信息:

  • 题目链接:https://leetcode.com/problems/evaluate-division/description/
  • 题目类型: Array,Graph, DFS,Union-Find (没错可以用 uf,但是今天暂时不分享这种做法)
  • 题目难度:Medium (但其实真的可以算是 hard,在面试中如果第一次碰到这个题就自求多福啦 ~~)
  • 题目来源:Google 高频面试题

题目问题:

  • 给定一个数组 pairs(equations)每一个 pair 的第一个是除数,第二个是被除数,均由 string 变量名指代)
  • 给定一个数组(values),每一个位置的 double 值对应上一数组 pairs 的相除结果
  • 给定一个数组 pairs(queries),要求输出每一个 pair 的第一个数 / 第二个数的结果,依次按序输出
  • 如果无法计算,则结果为 -1.0
  • 举例:

题目想法:

这道题的难点在于,怎么样将一个看似与图论无关的问题转化为 图论问题,或者说,在我们刚拿到这道题的时候,我们是怎么样去思考,从而将一个看似非常独立的题目转化为我们熟悉的解题结构中呢?

我们首先观察给定的题目特性,假设我们拥有 a/b,b/c 和他们对应的值,那我们是怎么得到 a/c 的值的呢:

a/c = a/b * b/c where a/b and b/c are given known value

我们可以发现 a/c 其实就是 a/b 和 b/c 以乘法链接在一起,展现出来的样子相当于先从 a -> b, 再从 b -> c, 诶,是不是感觉好像有图论的感觉了

如果我们要知道,比如说 a/d 的值,我们就需要找到以 a 为起点,d 为重点的一个非循环路径,而在每一次路径的过程中,分子为起点,分母为终点,而每次的结果都是以乘法叠加在一起的:

a/d = a/n1 * n1/n2 * n2/n3 ..... * nk/ d

这其中的 n1, n2, n3, ..., nk 必须满足:

  • 任意 n1/n2 的值已知,即要么已经给定,要么存在于图论系统中
  • 任意 n1 != n2,一旦有循环则意味着我们不可能抵达终点

 而经过这样的转化,我们的问题就变成了,对于每一个 query 的 a/b,我们以 a 为起点,通过遍历图,找寻是否能抵达 b 这个终点。如果不能抵达返回 -1。没经过一个节点,都将当前的值乘加在总的结果上,当我们找到最后一个点时,返回所有的乘法累计结果。

总的来说:在创造这个图的时候,两点之间的连接是双向的,而这个路径上的权重则为 a/b (如果是 a -> b) 或者是 b/a (如果是 b -> a), 而在题目给定 a/b 的结果后,b/a 就是 1/(a/b))

使用 DFS 来对每一个 起点 a 和终点 b 的结果进行找寻即可

题目解法:

  • 遍历所有 “equations”“values”,对于每一个位置,分别记录下来 a -> b 和 b -> a 的adjacent 和 相应的经过需要乘算的数。
  • 遍历所有需要找寻的 “queries”:
    • 以当前 query 的起始点与末端点找寻 DFS
    • 如果通路,返回结果
    • 如果不同,返回 -1

题目代码:

class Solution {
public:
    //store the divisor and dividend's mutual path
    unordered_map<string, vector<pair<string, double>>> graph;
    set<string> visited;
    vector<double> res;
    
    //traverse the graph for a single query
    double DFS(string start, string target, double currProduct){
        //if we visited this node, then we find the loop, return:
        if(visited.find(start) != visited.end()){
            return -1.0;   //indicate that this route is not ok
        }
        
        visited.insert(start);
        
        //if we reach the end:
        if(start == target){
            return currProduct;
        }
        
         //else, go over teh map:
        vector<pair<string, double>> neighbours = graph[start];
        for(auto neighbour: neighbours){
            string new_start = neighbour.first;
            double new_product = currProduct * neighbour.second;
            double res = DFS(new_start, target, new_product);
            if(res != -1.0){
                return res;
            }
        }
        return -1.0;
    }
    
    vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
        //copy the graph down
        //for Numerator -> Denominater, it just the values at the position
        //for Denominater -> Numerator, it is the 1 / (values at the position)
        int size = values.size();
        for(int i = 0; i < size; i++){
            graph[equations[i][0]].push_back({equations[i][1], values[i]});
            graph[equations[i][1]].push_back({equations[i][0], 1.0 / values[i]});
        }
        
        //traverse the route for each queries:
        for(int i = 0; i < queries.size(); i++){
            // if one of the point not in the map, then there is no route
            if(!graph.contains(queries[i][0]) || !graph.contains(queries[i][1])){
                res.emplace_back(-1.0);
                continue;
            }
            
            // if the start and destination is the same node, then it should be a self loop
            // answer shall be one
            if(queries[i][0] == queries[i][1]){
                res.emplace_back(1.0);
                continue;
            }
            
            //traverse the graph and append the answer, clear the map everytime we did it
            double ret = DFS(queries[i][0], queries[i][1], 1.0);
            res.emplace_back(ret);
            visited.clear();
        }
        return res;
    }
};
  • Runtime: O(M*N) DFS 遍历最差是需要全部遍历 O(N) * 总共有 M 组 query O(M)
  • Space: O(N) 存储所有点的adjacent 信息

http://www.niftyadmin.cn/n/5664848.html

相关文章

Spring Boot项目:多模块还是单模块?架构师的一次深思熟虑!

在一个阳光明媚的下午&#xff0c;作为一名软件架构师&#xff0c;你正在一边喝着咖啡&#xff0c;一边思索着一个问题&#xff1a;Spring Boot项目到底是多模块好&#xff0c;还是单模块好呢&#xff1f; 这并不是一个简单的技术选择&#xff0c;它关系到整个项目的架构走向、…

Longman Dictionary of Contemporary English (朗文当代高级英语辞典)

Longman Dictionary of Contemporary English {朗文当代高级英语辞典} 1. Longman Dictionary of Contemporary English1.1. school References 1. Longman Dictionary of Contemporary English https://www.ldoceonline.com/ 1.1. school https://www.ldoceonline.com/dicti…

二叉树的广度优先遍历和题目

二叉树广度优先遍历利用队列 。 typedef char BTDataType; typedef struct BinaryTreeNode {BTDataType data;struct BinaryTreeNode* left;struct BinaryTreeNode* right; }BTNode; typedef BTNode* QDataType;// 链式结构&#xff1a;表示队列 typedef struct QueueNode {…

深入理解Go语言中的并发封闭与for-select循环模式

在现代编程中,并发已经成为提高程序性能和响应能力的关键手段。然而,在并发环境下,如何安全地访问和操作共享数据却是一大挑战。本文将深入探讨Go语言中的**封闭(confinement)**技术,以及常见的for-select循环模式,帮助您编写更高效、更安全的并发代码。 并发编程中的安…

linux-Shell 编程-常用 Shell 脚本技巧

Linux Shell 编程&#xff1a;常用 Shell 脚本技巧 一、概述 Shell 脚本是 Linux 系统管理员和开发人员日常自动化任务的重要工具。通过编写 Shell 脚本&#xff0c;用户可以自动化重复性工作、简化系统维护、管理服务器资源等。Shell 脚本的强大之处在于其简洁和灵活性&…

Spring Boot-依赖冲突问题

Spring Boot 依赖冲突问题及其解决方案 1. 引言 在Spring Boot项目中&#xff0c;依赖管理是一个至关重要的环节。Spring Boot通过自动配置和强大的依赖管理简化了应用开发&#xff0c;但随着项目规模扩大和依赖数量的增加&#xff0c;依赖冲突问题常常会浮现。依赖冲突不仅会…

FreeSql 全面指南:从基础到高级实战,深入解析读写分离与导航属性

FreeSql 使用详解&#xff1a;从入门到高级 FreeSql 是一个开源的、轻量级的 ORM 框架&#xff0c;它为 .NET 开发人员提供了丰富的功能&#xff0c;包括 CRUD 操作、读写分离、多租户、导航属性支持等。相比于 Entity Framework Core&#xff0c;FreeSql 在性能和特性上有一些…

纯血鸿蒙NEXT常用的几个官方网站

一、官方文档 https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/Readme-CN.md刚入门查看最多的就是UI开发模块&#xff0c;首先要熟悉组件使用 二、官方API参考 https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/development-i…