#P2649. 栏目死循环检测
          
                        
                                    
                      
        
              - 
          
          
                      1000ms
            
          
                      Tried: 174
            Accepted: 41
            Difficulty: 6
            
          
          
          
                       所属公司 : 
                              华为
                                
            
                        
              时间 :2025年1月8日-留学生
                              
                      
          
 
- 
                        算法标签>DFS          
 
栏目死循环检测
题解
题面描述
在视频应用 APP 中,有多个专区,每个专区可以包含多个栏目,称为栏目的上架。专区是顶层栏目,父对象为 root,用 -1 表示。一个专区内只能包含栏目,不能包含其他专区。栏目和专区的名称互不重复,且同一栏目可以被多个不同的专区或父栏目上架。由于运营人员的误操作,可能会出现栏目之间的死循环,即在从根节点到叶子节点的遍历过程中,出现重复的栏目名称,导致系统执行代码时陷入死循环,最终引发系统宕机。题目要求设计一个程序检测系统中的所有栏目死循环,并将其路径打印出来。如果没有检测到死循环,则输出 NotFound。
思路
这道题的核心是通过图的深度优先搜索(DFS)来检测栏目的死循环。首先,将每个上架关系解析为图的邻接表形式,其中父节点指向子节点。在遍历过程中,使用DFS从根节点 -1 开始递归地探索每个路径,同时维护一个路径栈和一个访问集合,以检测路径中是否存在重复的栏目节点。如果发现重复节点,说明出现了死循环,记录当前路径并停止继续递归。最后,对所有死循环路径进行排序和去重,如果没有检测到任何死循环,输出 NotFound。
- 
输入解析:
- 输入字符串按 
;分割,得到多个上架关系。 - 每个上架关系可能包含多个父关系,用 
:分割。 - 每个父关系用 
,分割出子栏目和父栏目,构建图的邻接表(父节点到子节点的映射)。 
 - 输入字符串按 
 - 
构建图:
- 使用邻接表表示图,
parent -> list of children。 - 注意同一个子栏目可能有多个父栏目,需要处理多重父关系。
 
 - 使用邻接表表示图,
 - 
检测死循环:
- 使用深度优先搜索(DFS)从根节点 
-1开始遍历图。 - 在 DFS 过程中,维护一个路径栈和一个访问集合,用于检测当前路径中是否有重复的节点。
 - 当遇到已经在当前路径中的节点时,表示发现一个死循环,将当前路径记录下来。
 
 - 使用深度优先搜索(DFS)从根节点 
 - 
记录和输出循环路径:
- 将所有检测到的循环路径存储在一个容器中。
 - 最后,对所有路径按字典序排序,并逐行输出。
 - 如果没有检测到任何死循环,则输出 
NotFound。 
 
cpp
#include <bits/stdc++.h>
using namespace std;
// 函数:按多个分隔符分割字符串
vector<string> split(const string &s, const string &delims) {
    vector<string> tokens;
    size_t start = 0, pos;
    while ((pos = s.find_first_of(delims, start)) != string::npos) {
        if (pos > start)
            tokens.emplace_back(s.substr(start, pos - start));
        start = pos + 1;
    }
    if (start < s.length())
        tokens.emplace_back(s.substr(start));
    return tokens;
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    string input;
    // 读取整行输入
    getline(cin, input);
    
    // 按 ";" 和 ":" 分割输入,得到各个上架关系
    vector<string> relationships = split(input, ";:");
    
    // 构建父节点到子节点的映射关系
    unordered_map<string, vector<string>> parent_to_children;
    for(auto &rel : relationships){
        size_t comma = rel.find(',');
        if(comma == string::npos) continue; // 无效输入,但题目保证输入合法
        string child = rel.substr(0, comma);
        string parent = rel.substr(comma + 1);
        parent_to_children[parent].push_back(child);
    }
    
    vector<string> cycles; // 存储所有发现的循环路径
    // 当前路径和已访问节点集合
    vector<string> current_path;
    unordered_set<string> current_visited;
    
    // 存储所有循环路径
    vector<string> all_cycles;
    
    // 定义深度优先搜索的递归函数
    function<void(string)> dfs = [&](string node) {
        current_path.push_back(node);          // 将当前节点加入路径
        current_visited.insert(node);          // 标记当前节点为已访问
        if(parent_to_children.find(node) != parent_to_children.end()){
            for(auto &child : parent_to_children[node]){
                if(current_visited.find(child) != current_visited.end()){
                    // 发现循环,记录循环路径
                    string cycle_path;
                    for(auto &n : current_path){
                        cycle_path += n + "->";
                    }
                    cycle_path += child;
                    all_cycles.push_back(cycle_path);
                }
                else{
                    dfs(child); // 继续递归搜索子节点
                }
            }
        }
        current_path.pop_back();               // 回溯,移除当前节点
        current_visited.erase(node);           // 取消当前节点的访问标记
    };
    
    // 从根节点 "-1" 开始进行深度优先搜索
    dfs("-1");
    
    if(all_cycles.empty()){
        cout << "NotFound\n"; // 如果没有发现任何循环,输出 NotFound
        return 0;
    }
    
    // 按字典序对所有循环路径进行排序
    sort(all_cycles.begin(), all_cycles.end());
    
    // 输出每一条循环路径
    for(auto &cycle : all_cycles){
        cout << cycle << "\n";
    }
}
python
def split(s, delims):
    tokens = []
    start = 0
    while start < len(s):
        pos = next((i for i, char in enumerate(s[start:]) if s[start + i] in delims), -1)
        if pos == -1:
            tokens.append(s[start:])
            break
        if pos > 0:
            tokens.append(s[start:start + pos])
        start += pos + 1
    return tokens
def dfs(node, parent_to_children, all_cycles, current_path, current_visited):
    current_path.append(node)
    current_visited.add(node)
    if node in parent_to_children:
        for child in parent_to_children[node]:
            if child in current_visited:
                # 发现循环,记录循环路径
                cycle_path = "->".join(current_path) + "->" + child
                all_cycles.append(cycle_path)
            else:
                dfs(child, parent_to_children, all_cycles, current_path, current_visited)  # 继续递归搜索子节点
    current_path.pop()
    current_visited.remove(node)
def main():
    # 读取输入
    input_data = input().strip()
    
    # 按 ";" 和 ":" 分割输入,得到各个上架关系
    relationships = split(input_data, ";:")
    
    # 构建父节点到子节点的映射关系
    parent_to_children = {}
    for rel in relationships:
        comma = rel.find(',')
        if comma == -1: continue  # 无效输入,但题目保证输入合法
        child = rel[:comma]
        parent = rel[comma + 1:]
        if parent not in parent_to_children:
            parent_to_children[parent] = []
        parent_to_children[parent].append(child)
    
    # 存储所有循环路径
    all_cycles = []
    
    # 当前路径和已访问节点集合
    current_path = []
    current_visited = set()
    
    # 从根节点 "-1" 开始进行深度优先搜索
    dfs("-1", parent_to_children, all_cycles, current_path, current_visited)
    
    if not all_cycles:
        print("NotFound")  # 如果没有发现任何循环,输出 NotFound
    else:
        # 按字典序对所有循环路径进行排序
        all_cycles.sort()
        for cycle in all_cycles:
            print(cycle)
if __name__ == "__main__":
    main()
java
import java.util.*;
public class Main {
    // 函数:按多个分隔符分割字符串
    public static List<String> split(String s, String delims) {
        List<String> tokens = new ArrayList<>();
        int start = 0;
        while (start < s.length()) {
            int pos = -1;
            // 查找下一个分隔符的位置
            for (int i = 0; i < delims.length(); i++) {
                int tempPos = s.indexOf(delims.charAt(i), start);
                if (tempPos != -1 && (pos == -1 || tempPos < pos)) {
                    pos = tempPos;
                }
            }
            if (pos == -1) {
                tokens.add(s.substring(start));
                break;
            }
            if (pos > start) {
                tokens.add(s.substring(start, pos));
            }
            start = pos + 1;
        }
        return tokens;
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String input = scanner.nextLine();
        // 按 ";" 和 ":" 分割输入,得到各个上架关系
        List<String> relationships = split(input, ";:");
        // 构建父节点到子节点的映射关系
        Map<String, List<String>> parentToChildren = new HashMap<>();
        for (String rel : relationships) {
            int comma = rel.indexOf(',');
            if (comma == -1) continue; // 无效输入,但题目保证输入合法
            String child = rel.substring(0, comma);
            String parent = rel.substring(comma + 1);
            parentToChildren.computeIfAbsent(parent, k -> new ArrayList<>()).add(child);
        }
        // 存储所有循环路径
        List<String> allCycles = new ArrayList<>();
        // 当前路径和已访问节点集合
        List<String> currentPath = new ArrayList<>();
        Set<String> currentVisited = new HashSet<>();
        // 定义深度优先搜索的递归函数
        dfs("-1", parentToChildren, allCycles, currentPath, currentVisited);
        if (allCycles.isEmpty()) {
            System.out.println("NotFound"); // 如果没有发现任何循环,输出 NotFound
        } else {
            // 按字典序对所有循环路径进行排序
            Collections.sort(allCycles);
            for (String cycle : allCycles) {
                System.out.println(cycle);
            }
        }
    }
    public static void dfs(String node, Map<String, List<String>> parentToChildren,
                           List<String> allCycles, List<String> currentPath, Set<String> currentVisited) {
        currentPath.add(node);
        currentVisited.add(node);
        if (parentToChildren.containsKey(node)) {
            for (String child : parentToChildren.get(node)) {
                if (currentVisited.contains(child)) {
                    // 发现循环,记录循环路径
                    StringBuilder cyclePath = new StringBuilder();
                    for (String n : currentPath) {
                        cyclePath.append(n).append("->");
                    }
                    cyclePath.append(child);
                    allCycles.add(cyclePath.toString());
                } else {
                    dfs(child, parentToChildren, allCycles, currentPath, currentVisited); // 继续递归搜索子节点
                }
            }
        }
        currentPath.remove(currentPath.size() - 1);
        currentVisited.remove(node);
    }
}
        题目内容
视频 APP 里有多个专区,将栏目放到专区里成放到它栏目里,我们称为栏目的上架。专区是一种顶层栏目,里面只能放入栏目不能放入专区。专区的父对象就是 root ,我们用 −1 表示。栏目或专区里上架的栏目不能重名,所有专区不重名,也不存在栏目和专区重名,但同一个栏目可以上架到多个不同的专区或父栏目中。有时运营人员的误操作,会出现栏目的死循环。如果从栏目树的根节点往一个叶子节点遍历过程中,出现重复的栏目名称,则定义为一个栏目死循环。已知专区名称和栏目名称不会重复,且一条从根往叶子遍历的路径上最多只有一个死循坏。
如上图,有 2 处出现死循环,这种栏目上架死循环的数据会导致执行代码的死循环,引发系统宕机。因此为了避免出现这种情况,需要检测系统中的栏目死循坏。现在需要设计一个栏目树死循环检测程序,将检测到的所有死循环打印出来。

如上图,有2处出现死循环,这种栏目上架死循环的数据会导致执行代码的死循环,引发系统宕机。因此为了避免出现这种情况,需要检测系统中的栏目死循环。现在需要设计一个栏目树死循环检测程序,将检测到的所有死循环打印出来。
输入描述
输入是一行字符串,用分号;分隔多个栏目或专区上架关系。分号分隔后每一部分为逗号分隔的字符串,逗号左边是当前栏目或专区,逗号右边为要上架的父栏目或专区。图上的输入对应为
C1,−1;C2,−1;A,C1;B,C1;M,C2;N,C2;L,C2;A1,A;A2,A;A,N;A,A2;C,A2
输入分号的数量 <1000 ,逗号数量 >=1 ,所有输入都是合法输入,没有空白字符。输入字符串长度 <=100000.
输出描述
从根节点 −1 开始的包含死循环的路径,例如上图打印输出
−1−>C1−>A−>A2−>A
−1>C2−>N−>A−>A2−>A
多条找到的路径,打印顺序按每条路径的 String 字典升序输出
每个专区下可能有多个死循环,每条找到的路径打印一行记录,如果所有专区都没有找到死循环,则最终打印
NotFound
样例1
输入
C1,-1;C2,-1;A,C1;B,C1;M,C2;N,C2;L,C2;A1,A;A2,A;A,N;A,A2;C,A2
输出
-1->C1->A->A2->A
-1->C2->N->A->A2->A
说明
如题目描述,这里有 2 处死循环。如果一个本身有死循环的子树挂载到多个不同的节点上,则被挂载节点就会各自出现一个死循环。
样例2
输入
C1,-1;A,C1;A,A
输出
-1->C1->A->A
说明

这里栏目A里放入了自己,也是一种死循环
样例3
输入
C1,-1;A,C1;B,C1
输出
NotFound
说明

上图没有栏目死循环,故输出NotFound
提示
所有的栏目的祖先都是根节点,也就是向父节点遍历最终都可以遍历到−1。
在从根节点往叶子节点遍历过程中,出现死循环换后的栏目没有子栏目了。