回溯法理论基础 LeetCode 77. 组合 LeetCode 216.组合总和III LeetCode 17.电话号码的字母组合

news2025/5/18 15:50:18

目录

回溯法理论基础

回溯法

回溯法的效率

用回溯法解决的问题

如何理解回溯法

回溯法模板

LeetCode 77. 组合

回溯算法的剪枝操作

LeetCode 216.组合总和III

LeetCode 17.电话号码的字母组合


回溯法理论基础

回溯法

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯一般与递归相辅相成,并在递归后进行使用。回溯法是一种暴力搜索方法,你可以想象你现在在走迷宫,当你走到一个死路后,是否需要回退,而回退的这个过程就是回溯。

回溯法的效率

正如上面所说,回溯法是暴力搜索方法,其本质是穷举,穷举所有可能,然后选出我们想要的答案。如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

用回溯法解决的问题

那么既然回溯法并不高效为什么还要用它呢? 因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。
那什么问题适合用回溯法进行解决?

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

需要特别说明下组合和排列的问题,组合是没有顺序的,排列是有顺序,比如一对男女朋友,这是一个组合,但你问到他们之间谁先表白,这就变成排列了。

如何理解回溯法

回溯法解决的问题都可以抽象为树形结构

因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。有递归,就必须要有终止条件,所以必然是一棵高度有限的树(N叉树)。

回溯法模板

回溯法也有模板,在将其模版前,复习下递归的模板。

  1. 终止条件
  2. 递归顺序
  3. 输入参数和输出参数

回溯法模板。

1.回溯函数终止条件

既然回溯可以抽象为树形结构,那么也像递归二叉树那样存在终止条件。什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。

回溯函数终止条件伪代码如下:

if (终止条件) {
    存放结果;
    return;
}

2.回溯搜索的遍历过程

在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。

上图是特意举例集合大小和孩子的数量是相等的。size为4的集合有4个子集合。

回溯函数遍历过程伪代码如下:

for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    处理节点;
    backtracking(路径,选择列表); // 递归
    回溯,撤销处理结果
}

for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。

backtracking这里自己调用自己,实现递归

大家可以从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了

3.回溯函数模板返回值以及参数

回溯算法中函数返回值一般为void。

回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。

回溯函数伪代码如下:

void backtracking(参数)

回溯算法模板框架如下:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

涉及回溯算法的题都可以基于上述模板进行实现,回溯算法模板 + 题目特性 → 解决回溯算法问题

LeetCode 77. 组合

为什么要用回溯算法,你用两个for循环也可以做到,第一个循环从左到右逐个遍历,第二个从第一个循环的下标的下一个位置开始遍历,这样的话也可以做到,时间复杂度是O(N^2),那如果是k越大呢?此时不就需要更多个循环了吗,那此时时间复杂度就是O(N^k),显然时间复杂度太高。

回溯方法的做法其实就是用递归来替代循环。

思路:

  1. 通过for循环横向遍历起始处理节点(红箭头)
  2. 通过递归深度遍历可能出现的组合(蓝箭头)
  3. 递归后进行回溯
  4. 当组合长度 == k 时,为终止条件

手撕Code

class Solution(object):
    def combine(self, n, k):
        """
        :type n: int
        :type k: int
        :rtype: List[List[int]]
        """
        self.result = []    ## 二维数组
        self.path = []
        self.backtracking(n, k, 1)
        return self.result

    def backtracking(self, n, k, start_index):
        """
        n 记录需要处理到的最大数字
        k 记录需要输出的数组数目
        start_index 记录当前处理的数字, 遍历范围是1到n
        """

        ### 终止条件
        if len(self.path) == k:
            # self.result.append(self.path)     ### 这里是将指针给append了,self.path的内容是一直在变的,如果你将指针append的话,上一轮的递归的结果并没有进行保存,因为上一轮指向的数组已经变了。
            self.result.append(list(self.path))
            return

        ### 递归逻辑
        for _ in range(start_index, n+1):     ### 左闭右开
            self.path.append(start_index)     ### 当前处理节点。处理同一层的逻辑
            self.backtracking(n, k, start_index + 1)    ### 递归,往深度遍历。
            self.path.pop()                   ### 递归后进行回溯
            start_index += 1                  ### 处理完当前节点后,处理下一个节点

易错点:

不能是self.result.append(self.path) ,这部分append只是当前self.path指针所指向的当前数组元素,如果self.path当前指向的数组是[1,3],上一轮self.result已经将[1,2]给存进去了,此时如果你要将[1,3]数组进行append,你应该append是这个数组,而不应该是这个指针,即你想要的结果是[ [1,2], [1,3] ],如果你是append这个指针的话,只会添加当前指针指向的数组,因为你存储的其实是指向当前数组的指针,即[ self.path, self.path ],再进行下一轮的append操作后,其实你获取的是[ [1,3], [1,3] ]。指针指向的是不断在变的数组,如果你存指针的话,最终的self.result只会是存储操作完毕后该指针下的元素,没有记录整个过程。

总结:当你在终止条件 if len(self.path) == k: 中执行 self.result.append(self.path) 时,你并没有将 self.path 当前的内容复制一份,而是将 self.path 这个列表对象本身的引用添加到了 self.result 中。

回溯算法的剪枝操作

体现在for循环范围的限制。如还是组合问题,现在k变了,变为4。

那么我们可以看到由于元素只有4个,因此只有从元素1开始的遍历才能得到最终符合数目为k的组合,而其他子树的遍历是不需要的,因为肯定数目是不符合的。那如果k是3呢?是不是只有从1开始和从2开始进行遍历才是有效的,且其子遍历中,有些分支也可以进行去掉,从而减少遍历的操作,这是与通过多个循环暴力搜索法相比所体现出的优势。

那要如何优化以实现剪枝呢?其实答案就体现在for循环范围的控制。优化过程如下:

i = start_index,表示处理节点的起始位置。

  1. 已经选择的元素个数:path.size();
  2. 所需需要的元素个数为: k - path.size();
  3. 列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
  4. 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历。

根据3得到的是 i <= n - (k - path.size()) ,为什么要+1?因为包括起始位置,我们要是一个左闭的集合。

这个优化条件 i <= n - (k - path.size()) + 1 实际上是在说: "当前的起始位置 i 必须小于或等于这样一个值:从这个值开始,到最大的元素 n 结束,能够恰好凑齐我们所需要的 k - path.size() 个元素,并且还考虑到起始位置 i 本身也算一个元素。"        

n-i 是 指包含起始位置之后的列表元素个数,当n-i = k - path.size()时,此时有临界条件,i = n - (k - path.size),这表示i为这个值时,当前元素+后续元素刚好是符合k - path.size()的。但我们要的临界条件是i到了什么值时,后续遍历操作可以不用执行,因此要加1,1是表明i = n - (k - path.size)时,后序的元素可以进行遍历,再往后的就不用了。

举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。表示最大的符合条件在当第二个元素开始时。因此,从2开始。

class Solution(object):
    def combine(self, n, k):
        """
        :type n: int
        :type k: int
        :rtype: List[List[int]]
        """
        self.result = []    ## 二维数组
        self.path = []
        self.backtracking(n, k, 1)
        return self.result

    def backtracking(self, n, k, start_index):
        """
        n 记录需要处理到的最大数字
        k 记录需要输出的数组数目
        start_index 记录当前处理的数字, 遍历范围是1到n
        """

        ### 终止条件
        if len(self.path) == k:
            # self.result.append(self.path)     ### 这里是将指针给append了,self.path的内容是一直在变的,如果你将指针append的话,上一轮的递归的结果并没有进行保存,因为上一轮指向的数组已经变了。
            self.result.append(list(self.path))
            return

        for _ in range(start_index, (n -(k-len(self.path))+1) +1):      ### 剪枝操作
            self.path.append(start_index)     
            self.backtracking(n, k, start_index + 1)   
            self.path.pop()                  
            start_index += 1                 

LeetCode 216.组合总和III

思路

  • 与LeetCode组合类似,只不过这里是求和,判断是否有满足数目的path == sum
  • 思路基本一致,注意循环中不要忘了对start_index进行修改。

手撕Code

class Solution(object):
    def combinationSum3(self, k, n):
        """
        :type k: int
        :type n: int
        :rtype: List[List[int]]
        """
        self.result = []
        self.path = []
        self.nums = []
        for i in range(1,10):
            self.nums.append(i)         ### 创建一个数组用于存储用于遍历的数字
        
        self.backtracking(0, k, n)
        return self.result

    def backtracking(self, start_index, k, n):

        if len(self.path) == k:
            sum = 0
            for i in range(len(self.path)):
                sum += self.path[i]
            if sum == n:
                self.result.append(list(self.path))
            return

        for _ in range(start_index, len(self.nums)-(k-len(self.path)) +1):
            self.path.append(self.nums[start_index])
            self.backtracking(start_index+1, k, n)
            self.path.pop()
            start_index += 1

LeetCode 17.电话号码的字母组合

思路1 —— 朴实无华

  • 分级处理思想,之前操作是都是在同一个数组内,现在是多个数组,那只要修改回溯函数对应的输入和处理方法就行。具体地,输入两个数组,第一个数组进行横向遍历,第二个数据进行遍历。为什么在一个集合里使用start_index,是为了实现去重操作;在多个集合的操作,多个集合无交集的情况下,直接从0开始遍历,不用考虑去重操作。
  • 针对k=2时,传入两个数组就可以直接处理。而针对k=3,需要将k=2时的处理结果拿出来后,作为新的数组,与第三个数组输入到回溯函数中,从而实现k=3的计算逻辑。
  • k=4也是类似,就是逐步将前面的输出结果作为新的输入与下一个进行计算。

上述这种思路,跟用循环进行实现没什么区别了,没体现回溯的特点。另外,在处理数字为1的时候要注意,字符串不是像数组那般可以进行修改,因此当遇到1时,直接跳过就行,可以用一个new_digits来存储对旧digits进行判断修改后的结果。

class Solution(object):
    def letterCombinations(self, digits):
        """
        :type digits: str
        :rtype: List[str]
        """

        ### 采用一个Hashmap存储对应数字和字母的关系
        ### 对输入的数字进行判断已获得其对应的字母
        hash_map = dict()

        hash_map[2] = ['a','b','c']
        hash_map[3] = ['d','e','f']
        hash_map[4] = ['g','h','i']
        hash_map[5] = ['j','k','l']
        hash_map[6] = ['m','n','o']
        hash_map[7] = ['p','q','r', 's']
        hash_map[8] = ['t','u','v']
        hash_map[9] = ['w','x','y', 'z']

        ### 构造一个函数,第一个数组作为横向遍历,第二个数组作为纵向遍历
        self.result = []
        self.path   = [] 
        k = len(digits)

        # left = right = 0                #### python中的字符串是无法直接修改的,因此碰到1的话,应该是选择跳过,而不是进行修改
        # while right < k:    
        #     if digits[left] != '1':
        #         left += 1
        #         right += 1
        #     elif digits[left] == '1':
        #         right += 1
        #         digits[left] = digits[right] 
        cur = 0
        digits_list = []
        while cur < k:
            if digits[cur] != '1':
                digits_list.append(digits[cur])
            cur += 1
        
        new_digits = ''.join(digits_list)
        k = len(new_digits)

        if k == 0:
            return self.result
        if k == 1:
            return hash_map[int(digits[0])]

        first_str = hash_map[int(digits[0])]
        second_str = hash_map[int(digits[1])]
        if k == 2:
            self.backtracking(0, first_str, second_str, k)
        if k == 3:
            self.backtracking(0, first_str, second_str, 2)
            first_second_str = list(self.result)
            self.result = []
            third_str = hash_map[int(digits[2])]
            self.backtracking(0, first_second_str, third_str, 2)
        if k == 4:
            self.backtracking(0, first_str, second_str, 2)
            first_second_str = list(self.result)
            self.result = []
            third_str = hash_map[int(digits[2])]
            first_str = self.backtracking(0, first_second_str, third_str, 2)
            first_second_third_str = list(self.result)
            self.result = []
            forth_str = hash_map[int(digits[3])]
            self.backtracking(0, first_second_third_str, forth_str, 2)

        return self.result

    def backtracking(self, start_index, first_str, second_str, k):
        """
        first_str : 第一个字符串数组,横向遍历
        second_str : 第二个字符串数组,深度遍历
        k : path的长度 == digits.length
        """

        if len(self.path) == k:
            path_str = ''.join(self.path)
            self.result.append(path_str)
            return
        
        for _ in range(start_index, len(first_str)):
            self.path.append(first_str[start_index])
            self.backtracking(0, second_str, None, k)
            self.path.pop()
            start_index += 1 

思路2:

  • 关键还是在对回溯的理解,回溯的结构可以抽象成一颗树,在单个集合中,你是将单个元素分到树的每一层中。现在是多个集合,其实也就是把单个集合分到树的每一层中。
  • 在了解上述思想后,我们就可以设计index了,index表示的是digits的长度,其长度决定了有几个集合,有N个集合就决定了有N-1个回溯递归的深度(因为第一个是横向遍历)。

Code

class Solution(object):
    def letterCombinations(self, digits):
        """
        :type digits: str
        :rtype: List[str]
        """
        self.letterMap = [              ### 一个一维数组,数字的大小刚好对应去在数组中去获得该数字对应字符串的下标
            "",     # 0
            "",     # 1
            "abc",  # 2
            "def",  # 3
            "ghi",  # 4
            "jkl",  # 5
            "mno",  # 6
            "pqrs", # 7
            "tuv",  # 8
            "wxyz"  # 9
        ]

        self.result = []
        self.path = []
        new_digits = []

        for dig in digits:      ### 判断数字是否在[2,9]之内,如果有不存在的需要进行删除
            if dig != '1' or dig != '0':
                new_digits.append(dig)
        
        new_digits = "".join(new_digits)
        length = len(new_digits)        ### 数字的长度
        index = 0                       ### 树的深度

        if length == 0:
            return self.result

        self.backtracking(index, new_digits, length)
        return self.result
    
    def backtracking(self, index, new_digits, length):
        if index == length:
            self.result.append("".join(list(self.path)))
            return                         ### 终止条件
        
        cur = int(new_digits[index])       ### 当前处理的数字
        cur_str = self.letterMap[cur]       ### 当前处理的数字 对应的字符串

        for i in range (len(cur_str)):
            self.path.append(cur_str[i])
            self.backtracking(index+1, new_digits, length)
            self.path.pop()
            # index += 1           

注意,在self.path.pop()之后,index不需要+1,与start_index进行区分开来。start_index + 1是为了获得一个数组中后续的元素。而在不同的集合中,你index是操作不同集合,index为0传进后是第一层,如何往下进行深度遍历操作,已经在index+1作为输入参数进行输入时已经确定了,后序递归结束时,重新执行for循环,进行横向遍历,再一步递归去深度遍历。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2378602.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【进程控制二】进程替换和bash解释器

【进程控制二】进程替换 1.exec系列接口2.execl系列2.1execl接口2.2execlp接口2.3execle 3.execv系列3.1execv3.2总结 4.实现一个bash解释器4.1内建命令 通过fork创建的子进程&#xff0c;会继承父进程的代码和数据&#xff0c;因此本质上还是在执行父进程的代码 进程替换可以将…

JavaScript 的编译与执行原理

文章目录 前言&#x1f9e0; 一、JavaScript 编译与执行过程1. 编译阶段&#xff08;发生在代码执行前&#xff09;✅ 1.1 词法分析&#xff08;Lexical Analysis&#xff09;✅ 1.2 语法分析&#xff08;Parsing&#xff09;✅ 1.3 语义分析与生成执行上下文 &#x1f9f0; 二…

NHANES指标推荐:FMI

文章题目&#xff1a;Exploring the relationship between fat mass index and metabolic syndrome among cancer patients in the U.S: An NHANES analysis DOI&#xff1a;10.1038/s41598-025-90792-9 中文标题&#xff1a;探索美国癌症患者脂肪量指数与代谢综合征之间的关系…

【JDBC】JDBC常见错误处理方法及驱动的加载

MySQL8中数据库连接的四个参数有两个发生了变化 String driver "com.mysql.cj.jdbc.Driver"; String url "jdbc:mysql://127.0.0.1:3306/mydb?useSSLfalse&useUnicodetrue&characterEncodingutf8&serverTimezoneAsia/Shanghai"; 或者Strin…

车载以太网驱动智能化:域控架构设计与开发实践

title: 车载以太网驱动专用车智能化&#xff1a;域控架构设计与开发实践 date: 2023-12-01 categories: 新能源汽车 tags: [车载以太网, 电子电气架构, 域控架构, 专用车智能化, SOME/IP, AUTOSAR] 引言&#xff1a;专用车智能化转型的挑战与机遇 专用车作为城市建设与工业运输…

如何利用技术手段提升小学数学练习效率

在日常辅导孩子数学作业的过程中&#xff0c;我发现了一款比较实用的练习题生成工具。这个工具的安装包仅1.8MB大小&#xff0c;但基本能满足小学阶段的数学练习需求。 主要功能特点&#xff1a; 参数化出题 可自由设置数字范围&#xff08;如10以内、100以内&#xff09; 支…

BGP路由策略 基础实验

要求: 1.使用Preva1策略&#xff0c;确保R4通过R2到达192.168.10.0/24 2.用AS_Path策略&#xff0c;确保R4通过R3到达192.168.11.0/24 3.配置MED策略&#xff0c;确保R4通过R3到达192.168.12.0/24 4.使用Local Preference策略&#xff0c;确保R1通过R2到达192.168.1.0/24 …

第9讲、深入理解Scaled Dot-Product Attention

Scaled Dot-Product Attention是Transformer架构的核心组件&#xff0c;也是现代深度学习中最重要的注意力机制之一。本文将从原理、实现和应用三个方面深入剖析这一机制。 1. 基本原理 Scaled Dot-Product Attention的本质是一种加权求和机制&#xff0c;通过计算查询(Query…

双向长短期记忆网络-BiLSTM

5月14日复盘 二、BiLSTM 1. 概述 双向长短期记忆网络&#xff08;Bi-directional Long Short-Term Memory&#xff0c;BiLSTM&#xff09;是一种扩展自长短期记忆网络&#xff08;LSTM&#xff09;的结构&#xff0c;旨在解决传统 LSTM 模型只能考虑到过去信息的问题。BiLST…

MySQL UPDATE 执行流程全解析

引言 当你在 MySQL 中执行一条 UPDATE 语句时&#xff0c;背后隐藏着一套精密的协作机制。从解析器到存储引擎&#xff0c;从锁管理到 WAL 日志&#xff0c;每个环节都直接影响数据一致性和性能。 本文将通过 Mermaid 流程图 和 时序图&#xff0c;完整还原 UPDATE 语句的执行…

亚马逊云科技:开启数字化转型的无限可能

在数字技术蓬勃发展的今天&#xff0c;云计算早已突破单纯技术工具的范畴&#xff0c;成为驱动企业创新、引领行业变革的核心力量。亚马逊云科技凭借前瞻性的战略布局与持续的技术深耕&#xff0c;在全球云计算领域树立起行业标杆&#xff0c;为企业和个人用户提供全方位、高品…

【实测有效】Edge浏览器打开部分pdf文件显示空白

问题现象 Edge浏览器打开部分pdf文件显示空白或显示异常。 ​​​​​​​ ​​​​​​​ ​​​​​​​ 问题原因 部分pdf文件与edge浏览器存在兼容性问题&#xff0c;打开显示异常。 解决办法 法1&#xff1a;修改edge配置 打开edge浏览器&#x…

RJ连接器的未来:它还会是网络连接的主流标准吗?

RJ连接器作为以太网接口的代表&#xff0c;自20世纪以来在计算机网络、通信设备、安防系统等领域中占据了核心地位。以RJ45为代表的RJ连接器&#xff0c;凭借其结构稳定、信号传输可靠、成本低廉等优势&#xff0c;在有线网络布线领域被广泛采用。然而&#xff0c;在无线网络不…

Redis持久化机制详解:保障数据安全的关键策略

在现代应用开发中&#xff0c;Redis作为高性能的内存数据库被广泛使用。然而&#xff0c;内存的易失性特性使得持久化成为Redis设计中的关键环节。本文将全面剖析Redis的持久化机制&#xff0c;包括RDB、AOF以及混合持久化模式&#xff0c;帮助开发者根据业务需求选择最适合的持…

DeepSeek 大模型部署全指南:常见问题、优化策略与实战解决方案

DeepSeek 作为当前最热门的开源大模型之一&#xff0c;其强大的语义理解和生成能力吸引了大量开发者和企业关注。然而在实际部署过程中&#xff0c;无论是本地运行还是云端服务&#xff0c;用户往往会遇到各种技术挑战。本文将全面剖析 DeepSeek 部署中的常见问题&#xff0c;提…

嵌入式培训之数据结构学习(五)栈与队列

一、栈 &#xff08;一&#xff09;栈的基本概念 1、栈的定义&#xff1a; 注&#xff1a;线性表中的栈在堆区&#xff08;因为是malloc来的&#xff09;&#xff1b;系统中的栈区存储局部变量、函数形参、函数返回值地址。 2、栈顶和栈底&#xff1a; 允许插入和删除的一端…

RabbitMQ--进阶篇

RabbitMQ 客户端整合Spring Boot 添加相关的依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId> </dependency> 编写配置文件&#xff0c;配置RabbitMQ的服务信息 spri…

Android Studio报错Cannot parse result path string:

前言 最近在写个小Demo&#xff0c;参考郭霖的《第一行代码》&#xff0c;学习DrawerLayout和NavigationView&#xff0c;不知咋地&#xff0c;突然报错Cannot parse result path string:xxxxxxxxxxxxx 反正百度&#xff0c;问ai都找不到答案&#xff0c;报错信息是完全看不懂…

关于网站提交搜索引擎

发布于Eucalyptus-blog 一、前言 将网站提交给搜索引擎是为了让搜索引擎更早地了解、索引和显示您的网站内容。以下是一些提交网站给搜索引擎的理由&#xff1a; 提高可见性&#xff1a;通过将您的网站提交给搜索引擎&#xff0c;可以提高您的网站在搜索结果中出现的机会。当用…

基于QT(C++)OOP 实现(界面)酒店预订与管理系统

酒店预订与管理系统 1 系统功能设计 酒店预订是旅游出行的重要环节&#xff0c;而酒店预订与管理系统中的管理与信息透明是酒店预订业务的关键问题所在&#xff0c;能够方便地查询酒店信息进行付款退款以及用户之间的交流对于酒店预订行业提高服务质量具有重要的意义。 针对…