一步步到顶一个FTP服务器

本文详细介绍如何在Linux环境下安装和配置VSFTPD服务,包括网络配置、用户权限设置及解决常见问题等步骤。
问题1.
    最烦人的就是这个网络,三种方式。。。。。。算了吧。
实际用的就是最简单的桥接模式,不勾选复制物理网络连接状态。
启动虚拟机,配合WinScp与PuTTy修改配置文件完成网络配置

开工干活。。
    搭建ftp服务器,本来用的是别人的VM可惜字符集不对有些配置还不行。搞了半天还是自己的虚拟机好使。
yum install vsftpd 
安装完之后我们要对它进行配置,才能正常使用。编辑vsftpd的配置文件vi /etc/vsftpd/vsftpd.conf
vi编辑器中的搜索使命是斜杠“/”,然后输入要查找的内容,回车确定。以下是要更改的选项
anonymous_enable=NO  #禁止匿名访问
ascii_upload_enable #允许使用ascii码上传
ascii_download_enable #允许使用ascii码下载
userlist_deny=NO #(这条需手动添加到最后)使用FTP用户表,表里没有的用户需要添加才能登录
设置完毕之后,ESC,冒号wq回车。启动vsftpd服务/etc/init.d/vsftpd start,看到[确定]即为启动成功。
设置ftp权限
vi  /etc/vsftpd/vsftpd.conf 
将anonymous_enable=YES 改为 anonymous_enable=NO
ESC返回,输入“:wq”保存并退出

添加账号信息
useradd -d /home/test test //增加用户test,并制定test用户的主目录为/home/test
passwd test //为test设置密码
限制用户只能访问/home/test,不能访问其他路径

修改/etc/vsftpd/vsftpd.conf如下:
chroot_list_enable=YES //限制访问自身目录
chroot_list_file=/etc/vsftpd/vsftpd.chroot_list
编辑 vsftpd.chroot_list文件,将受限制的用户添加进去,每一个占用一行
重新启动VSFTP:
#service vsftpd restart //重启服务

问题2.
    忘记开始防火墙导致无法访问  
    停止防火墙 service iptables stop 
    启动防火墙 service iptables start 
    查看防火墙配置 iptables -L -n

问题3.
    [500 OOPS](http://www.2cto.com/os/201305/215684.html):cannot change directory 用户没有权限或参数设置错误
  sestatus -b| grep ftp 查看状态
  setsebool ftp_home_dir on 修改参数
// 上传 
public static void testUpload(String ip, int port, String userName,
            String passWord,String pathName, byte[] bs, String nameFile) {
        FTPClient ftpClient = new FTPClient();
        InputStream inputStream = null;
        inputStream = new ByteArrayInputStream(bs);
        System.out.println("上传文件....");
        try {
            ftpClient.connect(ip);
            ftpClient.setDefaultPort(port);
            ftpClient.login(userName, passWord);
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
            ftpClient.makeDirectory(pathName);
            ftpClient.changeWorkingDirectory(pathName);
            ftpClient.setBufferSize(1024);
            ftpClient.setControlEncoding("GBK");
             if (bs != null) {
                 ftpClient.storeFile(nameFile,inputStream);
             }
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("FTP客户端出错!", e);
        } finally {
            IOUtils.closeQuietly(inputStream);
            System.out.println("文件上传成功··");
            try {
                ftpClient.disconnect();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException("关闭FTP连接发生异常!", e);
            }
        }
    }
/**
     * 文件下载
     * @param ip
     * @param port
     * @param userName
     * @param passWord
     * @param pathName
     * @param filepath
     * @param nameFile
     * @param request
     * @param response
     */
    public static void testDownload(String ip, int port, String userName,
            String passWord, String pathName, String FileName, String realName,HttpServletRequest request,
            HttpServletResponse response) {
        FTPClient ftpClient = new FTPClient();
        FileOutputStream fos = null;
        System.out.println("下载文件....");
        try {
            ftpClient.connect(ip);
            ftpClient.setDefaultPort(port);
            ftpClient.login(userName, passWord);
            ftpClient.changeWorkingDirectory(pathName);// 转移到FTP服务器目录
            int reply;
            // 如果采用默认端口,可以使用ftp.connect(url)的方式直接连接FTP服务器
            reply = ftpClient.getReplyCode();
            if (!FTPReply.isPositiveCompletion(reply)) {
                ftpClient.disconnect();
            }
            FTPFile[] fs = ftpClient.listFiles();
            for (FTPFile ff : fs) {
                if (ff.getName().equals(realName)) {

                    File localFile = new File(realName);
                    OutputStream is = new FileOutputStream(localFile);
                    response.setContentType("text/html;charset=UTF-8");
                    request.setCharacterEncoding("UTF-8");
                    BufferedInputStream bis = null; 
                    BufferedOutputStream bos = null;
                    String checkBrowse =request.getHeader("user-agent");//request.getHeader(“User-agent”)返回客户端浏览器的版本号、类型
                    String FIREFOX ="Firefox";
                    Pattern p =Pattern.compile(FIREFOX,Pattern.MULTILINE);  
                    Matcher m=p.matcher(checkBrowse);
                    boolean is1  = m.find(); 
                    String fileName = new String(FileName.getBytes("utf-8"), "ISO8859_1");  
                    if(!is1){
                        fileName = URLEncoder.encode(FileName,"UTF-8");         
                    }
                    response.setContentType("application/binary;charset=ISO8859-1");
                    response.setHeader("Content-disposition", "attachment; filename="+ fileName);
                    request.setCharacterEncoding("UTF-8");
                    response.setHeader("Content-Length", String.valueOf(localFile.length()));

                        bis = new BufferedInputStream(new FileInputStream(realName));

                        bos = new BufferedOutputStream(response.getOutputStream());
                        byte[] buff = new byte[2048];
                        int bytesRead;
                        while (-1 != (bytesRead = bis.read(buff, 0, buff.length))) {
                            bos.write(buff, 0, bytesRead);
                        }
                        bis.close();
                        bos.close();
                    is.close();
                    break;
                }
            }
            ftpClient.logout();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("FTP客户端出错!", e);
        } finally {
            IOUtils.closeQuietly(fos);
            System.out.println("文件下载成功····");
            try {
                ftpClient.disconnect();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException("关闭FTP连接发生异常!", e);
            }
        }
    }
<think>我们已经在之前的对话中详细讲解了Kosaraju算法的原理和步骤。现在用户要求一个具体的示例,我们需要用一个完整的有向图例子来演示Kosaraju算法的执行过程。 示例图:我们使用之前提到过的图(但需要明确节点和边): 节点:0,1,2,3,4,5 边:0->1, 1->2, 1->3, 2->5, 5->4, 4->1, 4->3, 3->0 注意:这个图实际上是一个有向图,但边的关系需要明确。我们按照用户之前提供的图: 0 → 1 → 2 ↑ ↙ ↘ ↓ 3 ← 4 ← 5 实际上,这个图的结构是: 0 -> 1 1 -> 2, 1 -> 3 3 -> 0 2 -> 5 5 -> 4 4 -> 1, 4 -> 3 但是用户之前的描述中,边的关系是: 0→1, 1→2, 1→3, 2→5, 5→4, 4→1, 4→3, 3→0 因此,我们将使用这个边集合。 步骤: 1. 第一次DFS(在原图上): 我们需要得到节点的完成时间(后序遍历顺序)。 2. 构建转置图(将边反向)。 3. 第二次DFS(在转置图上,按照第一次DFS完成时间的逆序)。 然后,我们根据第二次DFS的遍历结果,每次从一个未访问的节点开始DFS所得到的连通块就是一个强连通分量。 我们将逐步执行并给出中间结果。 注意:DFS的遍历顺序可能不唯一,因为邻接表存储时,节点的邻居顺序会影响遍历顺序。但为了示例清晰,我们按照节点编号从小到大的顺序遍历邻居(除非有特别说明)。 开始: 原图(邻接表): 0: [1] 1: [2, 3] 2: [5] 3: [0] 4: [1, 3] # 注意:节点4的边是从5->4,所以原图中4有出边到1和3(因为转置前4的出边是1和3) 5: [4] 但是,我们之前写的边是:0->1, 1->2, 1->3, 2->5, 5->4, 4->1, 4->3, 3->0 所以邻接表应该是: 0: [1] 1: [2, 3] 2: [5] 3: [0] 4: [1, 3] # 因为4->1和4->3 5: [4] 第一次DFS(原图): 我们以任意未访问节点开始,假设从0开始(但DFS顺序可能影响完成顺序,但最终SCC结果相同)。 具体DFS过程: 从0开始: 0 -> 1 (0访问1) 1 -> 2 (1访问2) 2 -> 5 (2访问5) 5 -> 4 (5访问4) 4 -> 1 (1已访问) 和 4->3 (3未访问) -> 访问3 3 -> 0 (0已访问) -> 回溯 然后依次回溯:3完成,4完成,5完成,2完成,1完成,0完成。 所以完成顺序(栈)是:3,4,5,2,1,0 (后序遍历,先完成的后进栈,所以栈从底到顶是0,1,2,5,4,3?不对,实际代码中我们是完成一个节点就压栈,所以栈顶是最后完成的。但通常我们记录的顺序是完成时间从早到晚,栈顶是最后完成的。在第二次DFS时我们按栈的顺序(即从栈顶开始)就是逆后序。 实际上,在DFS中,我们递归返回时压栈,所以: 3先完成(压入3),然后4完成(压入4),再5完成(压入5),再2完成(压入2),再1完成(压入1),再0完成(压入0)。 所以栈(从底到顶):3,4,5,2,1,0 -> 当我们pop时,顺序是0,1,2,5,4,3。 我们也可以从其他节点开始,但为了覆盖所有节点,我们可能需要在主循环中按顺序选取未访问节点。但在这个例子中,从0开始DFS就访问了所有节点。 所以栈:stack = [3,4,5,2,1,0] (栈底到栈顶),pop顺序是0,1,2,5,4,3。 第二步:构建转置图 原图的边反向: 0->1 -> 1->0 1->2 -> 2->1 1->3 -> 3->1 2->5 -> 5->2 5->4 -> 4->5 4->1 -> 1->4 4->3 -> 3->4 3->0 -> 0->3 所以转置图的邻接表: 0: [3] # 原图中有3->0,所以转置图0有来自3的边,即0->3反向为3->0,转置图中0的入边变为出边:所以0的出边指向3(但实际转置图是:原图的边反向,所以转置图中0指向谁?转置图的边是原边的反向,所以: 原边:u->v,转置图边:v->u。 因此转置图: 0: [3] (因为原图有3->0,所以转置图0->3?不对,应该是:原图3->0,在转置图中应该是0->3?不对,转置图的定义是:将原图每条边反向。所以原图3->0,在转置图中是0->3。但通常我们这样构建:对于原图的边(u,v),在转置图中添加边(v,u)。 所以: 0: 原图有入边3->0,所以转置图中0的出边是3?不对,转置图中0的出边应该指向原图中指向0的节点,即节点3。所以0的出边:3。 我们重新构建转置图(按原图每条边反向): 原边:0->1 -> 转置:1->0 1->2 -> 2->1 1->3 -> 3->1 2->5 -> 5->2 5->4 -> 4->5 4->1 -> 1->4 4->3 -> 3->4 3->0 -> 0->3 因此转置图邻接表: 0: [3] # 原图3->0的边反向为0->3?不对,应该是:原图3->0,在转置图中应该表示为:有一条边从0指向3?不对,转置图的边应该是反向,所以原图的终点变为起点,起点变为终点。也就是说,转置图中,节点0的邻居是:原图中所有以0为终点的边的起点。所以原图中0的入边来自3(3->0),所以转置图中0的出边指向3。同理: 1: [0,4] # 原图0->1和4->1,所以转置图中1的出边指向0和4(即1->0和1->4)?不对,应该是:原图中指向1的边的起点是0和4,所以在转置图中,1的出边应该指向0和4吗?不,转置图的边是原图边的反向。所以原图0->1,在转置图是1->0;原图4->1,在转置图是1->4。所以1的出边是0和4。 2: [1,5] # 原图1->2,反向2->1;原图没有其他指向2的边?注意原图5->2?没有,原图2->5,所以转置图5->2,但5->2这条边在转置图中是从2指向5?不对,转置图是原边的反向,所以原图2->5,转置图5->2。那么2的入边就是5->2,所以转置图中2的出边指向5?不对,转置图中2的邻居应该是:原图中指向2的节点。原图中指向2的节点是1(1->2),所以在转置图中,2的出边指向1(因为原图1->2,反向为2->1)。另外,原图2->5,所以转置图中5->2,那么5的出边指向2,但这里我们构建的是每个节点的出边列表。所以节点2在转置图中的出边只有1(来自原图1->2的反向边2->1)。但是,原图还有边指向2吗?没有。所以2的出边只有1(即2->1)。为什么上面写5?不对。 重新整理: 我们按原图每条边,在转置图中添加反向边: 原边:0->1 -> 转置边:1->0 原边:1->2 -> 转置边:2->1 原边:1->3 -> 转置边:3->1 原边:2->5 -> 转置边:5->2 原边:5->4 -> 转置边:4->5 原边:4->1 -> 转置边:1->4 原边:4->3 -> 转置边:3->4 原边:3->0 -> 转置边:0->3 所以转置图: 0: [1, 3] # 因为转置边1->0和3->0?不对,我们添加了1->0和0->3?不对,我们添加的是: 转置边1->0,所以0是1的邻居?不对,我们在构建转置图时,对于原边(u,v),在转置图中添加一条边v->u。所以: 原边0->1 -> 转置边:1->0 -> 所以在转置图中,1有一条边指向0(即1的邻居有0)。 原边1->2 -> 转置边:2->1 -> 转置图中2的邻居有1。 原边1->3 -> 转置边:3->1 -> 转置图中3的邻居有1。 原边2->5 -> 转置边:5->2 -> 转置图中5的邻居有2。 原边5->4 -> 转置边:4->5 -> 转置图中4的邻居有5。 原边4->1 -> 转置边:1->4 -> 转置图中1的邻居有4(注意,这里1的邻居已经有0了,现在加上4)。 原边4->3 -> 转置边:3->4 -> 转置图中3的邻居有4。 原边3->0 -> 转置边:0->3 -> 转置图中0的邻居有3。 因此,转置图的邻接表为: 0: [3] # 原图3->0的反向边是0->3,但按照转置图的构建,我们添加了0->3?不对,应该是:原边3->0,转置图添加0->3?不对,我们构建转置图时,对于原边(u,v)添加边(v,u)。所以原边3->0,u=3,v=0,则添加边(0,3)?不对,应该是(v,u)=(0,3)。也就是说,我们添加一条从0指向3的边。所以0的邻居列表中添加3。 1: [0, 4, 2] # 为什么有2?不对。重新检查: 原边0->1:转置边1->0 -> 所以1的邻居添加0。 原边4->1:转置边1->4 -> 所以1的邻居添加4。 原边1->2:转置边2->1 -> 这条边是2指向1,所以是2的邻居添加1,而不是1的邻居添加2。 所以1的邻居只有0和4?不对,还有从其他节点指向1的边吗?没有。所以1的邻居:0,4。 修正: 节点0:原图有3->0,所以转置图0有一条边指向3(即0->3)?不对,转置图的边是原边的反向,所以原图3->0,转置图应该是0->3。所以0的出边是3。 节点1:原图有0->1(转置图1->0)和4->1(转置图1->4),所以1的出边是0和4。 节点2:原图有1->2(转置图2->1)和5->2?原图没有5->2,只有2->5(转置图5->2)。所以2的出边只有1(因为转置图2->1)。 注意:原图2->5,转置图5->2,所以5的出边有2,但2的入边来自5(转置图中5->2,所以2的出边并不包括5,因为转置图2的邻居应该是原图中指向2的节点,即1(原图1->2)和?没有其他指向2的边了。所以2的出边只有1(即2->1)。 节点3:原图有1->3(转置图3->1)和4->3(转置图3->4),所以3的出边是1和4。 节点4:原图有5->4(转置图4->5)和4->1(转置图1->4)?不对,4->1已经在上面处理了(在1的邻居里)。另外,原图5->4,转置图4->5,所以4的出边是5。 但是原图中4还有出边4->3(转置图3->4),所以4的出边还有3?不对,转置图4的出边应该是原图中指向4的节点。原图中指向4的节点只有5(5->4),所以转置图4的出边是5(4->5)。而原图4的出边(4->1和4->3)在转置图中变成了1->4和3->4,所以在转置图中4的入边来自1和3,但出边是5。 所以4的出边只有5。 节点5:原图有2->5(转置图5->2),所以5的出边是2。 因此,转置图邻接表: 0: [3] # 0->3 1: [0, 4] # 1->0, 1->4 2: [1] # 2->1 3: [1, 4] # 3->1, 3->4 (原图1->3和4->3,转置图3指向1和4?不对,转置图的边是反向,所以原图1->3变成3->1,原图4->3变成3->4。所以3的出边是1和4。) 4: [5] # 4->5 5: [2] # 5->2 这里有一个矛盾:节点3的出边有1和4,但节点1的邻居有4(1->4),节点4的邻居有5(4->5),节点5的邻居有2(5->2),节点2的邻居有1(2->1)。所以这个转置图的结构是: 0->3 1->0, 1->4 2->1 3->1, 3->4 4->5 5->2 第三步:第二次DFS(在转置图上),按栈的逆序(栈为[3,4,5,2,1,0])pop的顺序:0,1,2,5,4,3。 初始化visited全为False。 从栈中pop节点0,访问0(标记已访问),然后DFS(0)在转置图中:0的邻居是3,访问3(未访问),然后DFS(3): 3的邻居是1和4: 访问1(未访问),DFS(1): 1的邻居是0(已访问)和4(未访问),所以访问4: 4的邻居是5(未访问),DFS(5): 5的邻居是2(未访问),DFS(2): 2的邻居是1(已访问),回溯 回溯 回溯 回溯 回溯 回溯 所以从0开始的DFS访问了节点0,3,1,4,5,2。这是一个连通块(即一个强连通分量?)。但是,我们期望的是三个强连通分量:{0,3}, {1,2,4,5}? 不对,因为0和3与其他节点并不连通?在转置图上DFS,我们访问了所有节点。这不对,因为我们期望的SCC是三个。 问题:我们第一次DFS得到栈[3,4,5,2,1,0](栈底到栈顶),pop顺序是0,1,2,5,4,3。然后从0开始DFS,在转置图中,0可以到达3(0->3),3可以到达1和4(3->1, 3->4),1可以到达0和4(1->0,1->4),4可以到达5(4->5),5可以到达2(5->2),2可以到达1(2->1)。所以整个图是连通的?但在原图中,强连通分量应该是:{0,1,3,4,5,2}是一个?不对,我们来看原图: 原图: 0->1->2->5->4->1(形成环1->2->5->4->1)和4->3->0->1,所以0,1,2,3,4,5都在一个SCC中?那为什么之前说SCC是三个?用户之前说输出SCC: [[0,3], [1,2], [4,5]] 是错误的?我们重新分析原图: 原图: 0->1 1->2, 1->3 3->0 2->5 5->4 4->1, 4->3 因此,从0出发:0->1->2->5->4->1(形成1,2,5,4的环),然后4->3,3->0,所以0,1,2,3,4,5都是连通的。实际上,整个图是一个强连通分量?不对,因为从2不能到达0?我们看: 2->5->4->1->3->0,所以2可以到达0。同样,0可以到达2:0->1->2。所以整个图是强连通的?但是用户之前说输出三个SCC,可能是图不同。 我们重新检查用户最初给出的图描述: 0 → 1 → 2 ↑ ↙ ↘ ↓ 3 ← 4 ← 5 这个图的边: 0->1 1->2 1->3 (图中1有两个箭头,一个指向2,一个指向3) 2->5 (图中2指向5) 5->4 (图中5指向4) 4->1 (图中4指向1)和4->3(图中4指向3)?用户描述中有两个箭头从4出发:一个到1,一个到3。 3->0 (图中3指向0) 所以整个图是强连通的?那么为什么算法示例输出三个SCC?可能是用户之前的描述有误。 实际上,如果整个图是强连通的,那么Kosaraju算法应该输出一个SCC包含所有节点。 但是,用户之前说输出SCC:[[0,3], [1,2], [4,5]],这显然不是强连通的。所以可能是图的结构不同。 我们重新考虑用户之前提供的结构: 0 → 1 → 2 ↑ ↙ ↘ ↓ 3 ← 4 ← 5 注意箭头:1->3和4->3,3->0,0->1;1->2,2->5,5->4,4->1。所以整个图是强连通的,因此应该只有一个SCC。 那么用户之前为什么说输出三个SCC?可能是笔误,或者是另一个图。 为了演示三个SCC,我们换一个图:比如经典的例子(算法导论中的例子): 节点:0,1,2,3,4,5 边:0->1, 0->2, 1->3, 2->3, 3->0, 3->4, 4->5, 5->4 这个图的SCC有:{0,1,2,3}和{4,5}(两个SCC)?不对,0,1,2,3构成一个SCC(因为0,1,2,3互相可达),4,5构成一个SCC。 但我们想要三个SCC。让我们用另一个图: 节点:0,1,2,3,4,5 边:0->1, 1->2, 2->0, 1->3, 3->4, 4->5, 5->3 这个图的SCC:{0,1,2}和{3,4,5}。 或者我们使用用户之前提供的例子(但可能他描述的图不是强连通的)?我们再看用户之前的问题: “给定有向图: 0 → 1 → 2 ↑ ↙ ↘ ↓ 3 ← 4 ← 5” 然后他说“输出SCC:[[0,3], [1,2], [4,5]]” 这暗示着0,3在一个SCC,1,2在一个SCC,4,5在一个SCC。那么图的结构应该是: 0<->3 (0->3 and 3->0) 1<->2 (1->2 and 2->1) 但用户没有2->1,而是2->5 4<->5 (4->5 and 5->4) 但是用户画了0->1, 1->2, 1->3, 2->5, 5->4, 4->1, 4->3, 3->0 所以0->1->3->0 是一个环,0,1,3应该在一个SCC;1->2->5->4->1 是一个环,所以1,2,5,4应该在一个SCC。但是0,1,3和1,2,5,4共享了1,所以整个图是连通的,应该是一个SCC。 因此,用户之前提供的输出[[0,3], [1,2], [4,5]] 是错误的。 为了得到三个SCC,我们换一个图,比如: 0->1, 1->2, 2->0, // SCC1: {0,1,2} 1->3, 3->4, 4->5, 5->3 // SCC2: {3,4,5} 但是1->3的边连接了两个SCC,所以3,4,5是SCC,0,1,2是SCC,但1->3的边是跨SCC的边,所以整个图有两个SCC。 我们想要三个SCC,可以再加一个孤立节点6,或者再加一个环: 0->1, 1->2, 2->0, // SCC1:0,1,2 3->4, 4->5, 5->3, // SCC2:3,4,5 6->6 // SCC3:6 但这样节点6没有连接。 或者用更复杂的图: 0->1, 1->2, 2->0, 1->3, 3->4, 4->3, // 这里3,4是一个SCC 4->5, 5->6, 6->5 // 5,6是一个SCC 这样就有三个SCC: {0,1,2}, {3,4}, {5,6} 但为了方便,我们使用一个简单的有向图:三个不相交的环,环之间没有边。 例如:0->0 (自环),1->2->1,3->4->5->3。 但这样太简单。或者用: 0->1->0, 2->3->2, 4->5->4 三个环。 但是用户可能希望看到非平凡的例子。 我们选用经典例子(三个SCC): 节点:0,1,2,3,4,5 边:0->1, 1->2, 2->0, // SCC1:0,1,2 1->3, 3->4, 4->5, 5->3, // SCC2:3,4,5 2->4 // 跨SCC的边 注意:这个图有SCC1: {0,1,2} 和 SCC2: {3,4,5},尽管有2->4和1->3的边,但因为SCC的定义是极大强连通子图,{0,1,2}内部强连通,{3,4,5}内部强连通,{0,1,2}和{3,4,5}之间不构成强连通(例如,3不能到达0,1,2),所以是两个SCC。 但我们要三个SCC,所以再加一个节点6,以及6->6,这样就有三个SCC。 为了简化,我们使用四个节点分成三个SCC的例子: 节点:0,1,2,3 边:0->1, 1->0, // SCC1:0,1 2->3, 3->2, // SCC2:2,3 0->2 // 跨SCC的边 这个图有两个SCC:{0,1}和{2,3}。 三个SCC的例子(六个节点): 节点:0,1,2,3,4,5 边:0->1, 1->0, // SCC1:0,1 2->3, 3->2, // SCC2:2,3 4->5, 5->4, // SCC3:4,5 0->2, 2->4 // 跨SCC的边 现在,我们使用这个图来演示Kosaraju算法。 原图邻接表: 0: [1, 2] # 0->1, 0->2 1: [0] # 1->0 2: [3,4] # 2->3, 2->4 3: [2] # 3->2 4: [5] # 4->5 5: [4] # 5->4 第一次DFS(原图): DFS顺序:我们从0开始: 0 -> 1 (访问1) 1 -> 0 (0已访问) -> 回溯 0 -> 2 (访问2) 2 -> 3 (访问3) 3 -> 2 (2已访问) -> 回溯 2 -> 4 (访问4) 4 -> 5 (访问5) 5 -> 4 (4已访问) -> 回溯 回溯 回溯 后序遍历压栈顺序: 1完成:压栈1 0完成:压栈0 -> 注意:0还有邻居2未访问,所以0不会在1完成后马上完成,而是先访问2的整个分支后再完成0。 实际递归过程: dfs(0): visit 0 -> mark visited visit neighbor 1 (not visited) -> dfs(1) dfs(1): visit 1 -> mark neighbor 0 (visited) -> return finish 1 -> push 1 visit neighbor 2 (not visited) -> dfs(2) dfs(2): visit 2 -> mark neighbor 3 (not visited) -> dfs(3) dfs(3): visit 3 -> mark neighbor 2 (visited) -> return finish 3 -> push 3 neighbor 4 (not visited) -> dfs(4) dfs(4): visit 4 -> mark neighbor 5 (not visited) -> dfs(5) dfs(5): visit 5 -> mark neighbor 4 (visited) -> return finish 5 -> push 5 finish 4 -> push 4 finish 2 -> push 2 finish 0 -> push 0 所以栈(从底到顶):1,3,5,4,2,0 -> pop顺序:0,2,4,5,3,1。 第二步:转置图 原图边: 0->1 -> 1->0 0->2 -> 2->0 1->0 -> 0->1 2->3 -> 3->2 2->4 -> 4->2 3->2 -> 2->3 4->5 -> 5->4 5->4 -> 4->5 所以转置图邻接表(我们只关心出边): 0: [1] # 原图1->0,转置图0->1;原图0->1,转置图1->0(所以0的出边有1,1的出边有0) -> 但转置图是我们自己构建的,按原边反向。 具体:原边0->1 -> 转置 1->0 原边0->2 -> 转置 2->0 原边1->0 -> 转置 0->1 原边2->3 -> 转置 3->2 原边2->4 -> 转置 4->2 原边3->2 -> 转置 2->3 原边4->5 -> 转置 5->4 原边5->4 -> 转置 4->5 所以转置图: 0: [1] # 来自原图1->0 (转置0->1) -> 0的出边1 1: [0] # 来自原图0->1 (转置1->0) -> 1的出边0 2: [0,3] # 来自原图0->2 (转置2->0) 和 3->2 (转置2->3) -> 2的出边0,3 3: [2] # 来自原图2->3 (转置3->2) -> 3的出边2 4: [2,5] # 来自原图2->4 (转置4->2) 和 5->4 (转置4->5) -> 4的出边2,5 5: [4] # 来自原图4->5 (转置5->4) -> 5的出边4 第三步:第二次DFS(转置图),按栈pop顺序:0,2,4,5,3,1。 初始化visited=[false]*6 pop0: 访问0,DFS(0)在转置图:0->1,访问1(未访问),DFS(1):1->0(0已访问) -> 回溯。得到SCC1: [0,1] pop2: 2未访问,DFS(2):2->0(0已访问)-> 2->3,访问3(未访问),DFS(3):3->2(2已访问)-> 回溯。得到SCC2: [2,3] pop4: 4未访问,DFS(4):4->2(2已访问)-> 4->5,访问5(未访问),DFS(5):5->4(4已访问)-> 回溯。得到SCC3: [4,5] pop5: 5已访问,跳过。 pop3: 3已访问,跳过。 pop1: 1已访问,跳过。 所以SCC: [0,1], [2,3], [4,5] 这符合预期。 因此,我们用这个例子来演示。 总结示例图: - 节点: 0,1,2,3,4,5 - 原图边: 0->1, 0->2, 1->0, 2->3, 2->4, 3->2, 4->5, 5->4 步骤: 1. 第一次DFS(原图): DFS顺序:0->1 (然后1->0,0已访问,回溯) -> 0->2->3 (然后3->2,2已访问,回溯) -> 2->4->5 (然后5->4,4已访问,回溯) 完成顺序(压栈): 1,3,5,4,2,0 (栈底到栈顶),pop顺序: 0,2,4,5,3,1 2. 转置图: 0: [1] # 0->1 1: [0] # 1->0 2: [0,3] # 2->0, 2->3 3: [2] # 3->2 4: [2,5] # 4->2, 4->5 5: [4] # 5->4 3. 第二次DFS(转置图): 按pop顺序: 0->1 -> SCC1: [0,1] 2->0(已访问), 2->3 -> SCC2: [2,3] 4->2(已访问), 4->5 -> 5->4 -> SCC3: [4,5] 输出SCC: [[0,1], [2,3], [4,5]] 现在,我们按照这个示例来写回答。 </think> ### Kosaraju算法示例详解 以下通过一个具体有向图逐步演示Kosaraju算法的执行过程。通过此例可直观理解算法如何将图分解为强连通分量(SCC)。 #### 示例图结构 - **节点**:`0,1,2,3,4,5` - **边**: ```mermaid graph LR 0 --> 1 0 --> 2 1 --> 0 2 --> 3 2 --> 4 3 --> 2 4 --> 5 5 --> 4 ``` - **邻接表表示**: ```python graph = [ [1, 2], # 0 ->1, 0->2 [0], # 1->0 [3, 4], # 2->3, 2->4 [2], # 3->2 [5], # 4->5 [4] # 5->4 ] ``` - **期望SCC结果**:`[0,1]`, `[2,3]`, `[4,5]`(三个强连通分量) #### 步骤演示 ##### 1. 第一次DFS(记录节点完成顺序) - **遍历顺序**(按节点编号递增访问邻居): - 从`0`开始:`0→1→0`(回溯)→`0→2→3→2`(回溯)→`2→4→5→4`(回溯) - **完成时间(压栈顺序)**: 节点完成递归时入栈 → 栈底到栈顶:`[1, 3, 5, 4, 2, 0]` (`1`最先完成,`0`最后完成) - **栈的pop顺序**:`0→2→4→5→3→1`(逆后序遍历) ##### 2. 构建转置图 - **边反向规则**:原边 $u \to v$ 转为 $v \to u$ - **转置图邻接表**: ```python transpose = [ [1], # 0: 原1->0 → 0->1 [0], # 1: 原0->1 → 1->0 [0, 3], # 2: 原0->2 和 3->2 → 2->0, 2->3 [2], # 3: 原2->3 → 3->2 [2, 5], # 4: 原2->4 和 5->4 → 4->2, 4->5 [4] # 5: 原4->5 → 5->4 ] ``` ##### 3. 第二次DFS(在转置图上按逆序搜索) - **按栈pop顺序访问**:`0→2→4→5→3→1` - **SCC识别过程**: | 当前节点 | 访问状态 | DFS过程 | 找到的SCC | |----------|----------------|---------------------------------|-----------| | `0` | 未访问 | `0→1`(递归访问`1`) | `[0,1]` | | `2` | 未访问 | `2→3`(递归访问`3`) | `[2,3]` | | `4` | 未访问 | `4→5`(递归访问`5`) | `[4,5]` | | `5` | 已访问(跳过) | - | - | | `3` | 已访问(跳过) | - | - | | `1` | 已访问(跳过) | - | - | #### 算法正确性验证 - **SCC1 [0,1]**:$0 \leftrightarrow 1$(双向可达) - **SCC2 [2,3]**:$2 \to 3 \to 2$(环状结构) - **SCC3 [4,5]**:$4 \to 5 \to 4$(环状结构) - 分量间无双向路径(如 $0 \to 2$ 但 $2 \not\to 0$),符合SCC定义[^1]。 #### 关键点说明 1. **逆序遍历的作用**: 首次DFS的逆序(栈pop顺序)保证转置图的DFS从"汇点"开始,避免跨SCC搜索[^3]。 2. **转置图性质**: 转置图与原图的SCC完全相同,但边反向改变了DFS的扩散方向。 3. **时间复杂度**: 两次DFS($O(|V|+|E|$) + 转置图构建($O(|E|$)),总复杂度 $O(|V|+|E|)$[^2]。 > 此例验证了Kosaraju算法通过两次DFS和转置图操作,高效分解有向图为强连通分量。实际应用中,该算法常用于社交网络社群检测、编译器循环分析等场景[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彳卸风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值