题意整理

  • 给定一个长度为n的排列p,以及m个数对,每个数对包含一个和一个
  • 可以将排列p中下标对应值和下标对应值互换,数对的使用顺序和次数不限。
  • 求任意次操作后字典序最小的排列p。

方法一(并查集)

1.解题思路

并查集简述:

并查集主要用于解决图中的连通域问题,起初并查集里包含n个节点,每个节点都有一个根,初始情况下,根指向自身。然后有两个关键方法,一个是find,一个是union。find用于查找当前节点的根,union用于连接两个节点,连接以后处于同一个连通域,即共享一个根。

  • 首先新建并查集,遍历Pair数组,连接对应节点。
  • 再新建两个邻接表,一个用于记录当前根对应连通域所有节点的下标,另一个用于记录当前根对应连通域所有节点的值。
  • 遍历所有可能的根,然后对连通域内的节点值排序,赋值到perm数组。直到遍历完所有的根。

动图展示:
图片说明

2.代码实现

import java.util.*;

/*
 * public class Point {
 *   int x;
 *   int y;
 * }
 */

public class Solution {
    /**
     * 返回牛牛所能得到的字典序最小的排列
     * @param n int整型 
     * @param m int整型 
     * @param perm int整型一维数组 
     * @param Pair Point类一维数组 
     * @return int整型一维数组
     */
    public int[] solve (int n, int m, int[] perm, Point[] Pair) {
        //新建并查集
        UF uf=new UF(n);
        //连接对应节点
        for(Point point:Pair){
            uf.union(point.x-1,point.y-1);
        }

        //初始化邻接表,分别记录可交换节点的Id和Value
        List<List<Integer>> IdGraph=new ArrayList<>();
        List<List<Integer>> ValueGraph=new ArrayList<>();

        for(int i=0;i<n;i++){
            IdGraph.add(new ArrayList<>());
            ValueGraph.add(new ArrayList<>());
        }

        for(int i=0;i<n;i++){
            IdGraph.get(uf.find(i)).add(i);
            ValueGraph.get(uf.find(i)).add(perm[i]);
        }

        for(int i=0;i<n;i++){
            //找到可交换的所有节点,将其Value进行排序
            Collections.sort(ValueGraph.get(i));
            for(int j=0;j<IdGraph.get(i).size();j++){
                //将排序后的节点写入perm排列,保证还是那些Value对应的Id
                perm[IdGraph.get(i).get(j)]=ValueGraph.get(i).get(j);
            }
        }
        return perm;
    }

}

//并查集结构
class UF{
    //父数组
    int[] parent;

    UF(int n){
        //初始化
        parent=new int[n];
        for(int i=0;i<n;i++){
            parent[i]=i;
        }
    }

    //连接p节点和q节点
    public void union(int p,int q){
        //首先找打对应根节点,然后将其中一个节点的父置为另一个节点
        int pRoot=find(p);
        int qRoot=find(q);
        if(pRoot==qRoot) return;
        parent[pRoot]=qRoot;

    }

    //寻找根节点
    public int find(int x){
        //如果根节点不是自身,则将其父节点置为父节点的父节点
        while(x!=parent[x]){
            parent[x]=parent[parent[x]];
            //继续沿父节点往上找
            x=parent[x];
        }
        return x;
    }
}

3.复杂度分析

  • 时间复杂度:最坏情况下,并查集union操作的时间复杂度为,需要进行m次,于是所有union操作的时间复杂度是,然后假设并查集最后有k个根,每个根连通域大小为,则时间复杂度为图片说明 ,所以最终的时间复杂度为图片说明
  • 空间复杂度:所有连通域的总节点个数最多为n,所以空间复杂度为

方法二(按秩合并)

1.解题思路

基本思路和方法一相同。对并查集的union操作进行了优化。原来的union方式会导致某些节点离根越来越远,从而find方法耗时越来越长。假设现在有两个连通域,一个连通域根是pRoot,大小是m,另一个连通域根是qRoot,大小是n,并且m<n。如果连接pRoot和qRoot,将pRoot作为新连通域的根,则会有n个节点的查找路径长度加一,如果将qRoot作为新连通域的根,则会有m个节点的查找路径长度加一,显然让qRoot作为新的根,对find效率的影响会更小一点。

2.代码实现

import java.util.*;

/*
 * public class Point {
 *   int x;
 *   int y;
 * }
 */

public class Solution {
    /**
     * 返回牛牛所能得到的字典序最小的排列
     * @param n int整型 
     * @param m int整型 
     * @param perm int整型一维数组 
     * @param Pair Point类一维数组 
     * @return int整型一维数组
     */
    public int[] solve (int n, int m, int[] perm, Point[] Pair) {
        //新建并查集
        UF uf=new UF(n);
        //连接对应节点
        for(Point point:Pair){
            uf.union(point.x-1,point.y-1);
        }

        //初始化邻接表,分别记录可交换节点的Id和Value
        List<List<Integer>> IdGraph=new ArrayList<>();
        List<List<Integer>> ValueGraph=new ArrayList<>();

        for(int i=0;i<n;i++){
            IdGraph.add(new ArrayList<>());
            ValueGraph.add(new ArrayList<>());
        }

        for(int i=0;i<n;i++){
            IdGraph.get(uf.find(i)).add(i);
            ValueGraph.get(uf.find(i)).add(perm[i]);
        }

        for(int i=0;i<n;i++){
            //找到可交换的所有节点,将其Value进行排序
            Collections.sort(ValueGraph.get(i));
            for(int j=0;j<IdGraph.get(i).size();j++){
                //将排序后的节点写入perm排列,保证还是那些Value对应的Id
                perm[IdGraph.get(i).get(j)]=ValueGraph.get(i).get(j);
            }
        }
        return perm;
    }

}

//并查集结构
class UF{
    //父数组
    int[] parent;
    //秩数组
    int[] size;

    UF(int n){
        //初始化
        parent=new int[n];
        size=new int[n];
        for(int i=0;i<n;i++){
            parent[i]=i;
            size[i]=1;
        }
    }

    //连接p节点和q节点
    public void union(int p,int q){
        //首先找打对应根节点,然后将其中一个节点的父置为另一个节点
        int pRoot=find(p);
        int qRoot=find(q);
        if(pRoot==qRoot) return;
        //将秩较小的根指向秩较大的根
        if(size[pRoot]<size[qRoot]){
            parent[pRoot]=qRoot;
        }
        else{
            parent[qRoot]=pRoot;
        }

    }

    //寻找根节点
    public int find(int x){
        //如果根节点不是自身,则将其父节点置为父节点的父节点
        while(x!=parent[x]){
            parent[x]=parent[parent[x]];
            //继续沿父节点往上找
            x=parent[x];
        }
        return x;
    }
}

3.复杂度分析

  • 时间复杂度:最坏情况下,并查集union操作的时间复杂度为,需要进行m次,于是所有union操作的时间复杂度是,然后假设并查集最后有k个根,每个根连通域大小为,则时间复杂度为图片说明 ,所以最终的时间复杂度为图片说明
  • 空间复杂度:所有连通域的总节点个数最多为n,所以空间复杂度为