• 序列中的交换问题


    一、逆序对系列问题

    题目:http://poj.org/problem?id=1804

    题意:给定一个序列a[],每次只允许交换相邻两个数,最少要交换多少次才能把它变成非递降序列.

    求逆序对的裸题。

    如果我们交换相邻两个数,我们逆序对的个数只能是+1或-1

    我们现在需要得到一个非递减数列,即消去所有逆序对,

    而我们需要最少交换次数,即统计原数组中逆序对个数。

    对于一个序列中,有Ai>Aj,i<j的两个元素,我们把这个二元组称为逆序对

    有常见的三种方法求逆序对

    1.n^2的冒泡

    2.树状数组

    可以把数一个个插入到树状数组中,每插入一个数,统计比他小的数的个数,对应的逆序为 i- getsum(data[i]),其中 i 为当前已经插入的数的个数, getsum(data[i])为比 data[i] 小的数的个数,i-getsum(data[i])即比 data[i] 大的个数,即逆序的个数。最后需要把所有逆序数求和,就是在插入的过程中边插入边求和。

      
    const maxn=200001;
    
    var val,hash,tree:array [0..maxn] of longint;
        n:longint;
    
    function lowbit(x:longint):longint;
    begin
      exit(x and (-x));
    end;
    
    function getsum(pos:longint):longint;
    var ans:longint;
    begin
        ans:=0;
        while pos>0 do 
            begin
                inc(ans,tree[pos]);
                dec(pos,lowbit(pos));
            end;
        exit(ans);
    end;
    
    procedure modify(pos:longint);
    begin
        while pos<=n do 
            begin
                inc(tree[pos]);
                inc(pos,lowbit(pos));
            end;
    end;
    
    procedure qsort(l,r:longint);
    var    i,j,t,p:longint;
    begin
        if l>=r then exit;
        i:=random(r-l+1)+l;
        t:=val[i];    p:=hash[i];
        val[i]:=val[l];    hash[i]:=hash[l];
        i:=l;        j:=r;
        while i<j do
            begin
                while (i<j)    and    (t<val[j]) do dec(j);
                if i=j then break;
                val[i]:=val[j];    hash[i]:=hash[j];
                inc(i);
                while (i<j)    and    (val[i]<t) do inc(i);
                if i=j then break;
                val[j]:=val[i];    hash[j]:=hash[i];
                dec(j);
            end;
        val[i]:=t; hash[i]:=p;
        qsort(l,i-1);
        qsort(i+1,r);
    end;
    
    procedure main;
    var i,ans,m:longint;
    begin    
        ans:=0; m:=0;
        randomize;
        read(n);
        for i:=1 to n do
            begin
                read(val[i]);
                hash[i]:=i;
            end;
        qsort(1,n);
        tree:=val;
        for i:=1 to n do
            if tree[i]<>tree[i-1] then
                begin
                    inc(m);
                    val[hash[i]]:=m;
                end
            else val[hash[i]]:=m;
        fillchar(tree,sizeof(tree),0);
        for i:=1 to n do
            begin
                inc(ans,getsum(m)-getsum(val[i]));
                modify(val[i]);
            end;
        writeln(ans);
    end;
    
    begin
        main;
    end.
    View Code

      3.归并排序 

      实际上归并排序的交换次数就是这个数组的逆序对个数,为什么呢?

      我们可以这样考虑:

      归并排序是将数列a[l,h]分成两半a[l,mid]和a[mid+1,h]分别进行归并排序,然后再将这两半合并起来。

      在合并的过程中(设l<=i<=mid,mid+1<=j<=h),当a[i]<=a[j]时,并不产生逆序数;当a[i]>a[j]时,在

      前半部分中比a[i]大的数都比a[j]大,将a[j]放在a[i]前面的话,逆序数要加上mid+1-i。因此,可以在归并排序中的合并过程中计算逆序数.

      在合并的时候设左数组为1~x,右数组为x+1~y,则当a[i]<a[j],(1<=i<=x,x+1<=j<=y)必定有a[i]>a[x+1]~a[j-1],于是它们都是逆序对。 

      
    const maxn=100001;
    
    var val,hash:array [0..maxn] of longint;
        ans:longint;
        
    procedure merge(l,mid,r:longint);
    var i,j,k:longint;
    begin
        i:=l; j:=m+1; k:=l;
        while (i<=m) and (j<=r) do
            begin
                if val[i]>val[j] then 
                    begin
                        hash[k]:=val[j];
                        inc(k);
                        inc(j);
                        inc(ans,m-i+1);
                    end
                else
                    begin
                        hash[k]:=val[i];
                        inc(k);
                        inc(i);
                    end;
            end;
    end;
    
    procedure merge_sort(l,r:longint);
    var mid:longint;
    begin
        if l<r then 
            begin
                mid:=(l+r)>>1;
                merge_sort(l,mid);
                merge_sort(mid+1,r);
                merge(l,mid,r);
            end;
    end;
    
    procedure main;
    var i,n:longint;
    begin
        read(n);
        for i:=1 to n do 
        read(a[i]);
        merge_sort(1,n);
        writeln(ans);
    end;
    
    begin
        main;
    end.
    View Code

    二、置换群系列问题

      题目:http://poj.org/problem?id=3270

      题意:给定一个序列a[],每次只允许交换任意两个数,最少要交换多少次才能把它变成非递降序列.

    看上去跟逆序对很像,但是我们发现,我们每交换一次,可能减少很多逆序对

    所以显然不是统计逆序对的题目。

    怎么搞呢?

    1.找出初始状态和目标状态。明显,目标状态就是排序后的状态。
    2.画出置换群,在里面找循环。例如,数字是8 4 5 3 2 7
    明显,目标状态是2 3 4 5 7 8,能写为两个循环:
    (8 2 7)(4 3 5)。
    3.观察其中一个循环,明显地,要使交换代价最小,应该用循环里面最小的数字2,去与另外的两个数字,7与8交换。这样交换的代价是:
    sum - min + (len - 1) * min
    化简后为:
    sum + (len - 2) * min
    其中,sum为这个循环所有数字的和,len为长度,min为这个环里面最小的数字。
    4.考虑到另外一种情况,我们可以从别的循环里面调一个数字,进入这个循环之中,使交换代价更小。例如初始状态:
    1 8 9 7 6
    可分解为两个循环:
    (1)(8 6 9 7),明显,第二个循环为(8 6 9 7),最小的数字为6。我们可以抽调整个数列最小的数字1进入这个循环。使第二个循环变为:(8 1 9 7)。让这个1完成任务后,再和6交换,让6重新回到循环之后。这样做的代价明显是:
    sum + min + (len + 1) * smallest
    其中,sum为这个循环所有数字的和,len为长度,min为这个环里面最小的数字,smallest是整个数列最小的数字。
    5.因此,对一个循环的排序,其代价是sum - min + (len - 1) * min和sum + min + (len + 1) * smallest之中小的那个数字。

    置换群是啥?

      一个置换可以写成若干循环的乘积,那么如果置换求幂的话,一个循环不会跑到另一个循环里面去。

      我们可以简单理解为这几个位置的数来回换。 

      
    const maxn=100001;
    
    type node=record
            val,cnt:longint;
    end;
    
    var n,minvalue:longint;
        sum,tot:int64;
        m,t:array [0..maxn] of longint;
        flag:array [0..maxn] of boolean;
        a:array [0..maxn] of node;
    
    function min(x,y:longint):longint; inline;
    begin
        if x<y then exit(x)
        else exit(y);
    end;
    
    procedure find(x:longint); inline;
    var i:longint;
    begin
        for i:=0 to n-1 do
            begin
                if (t[i]=x) and (not flag[i]) then
                    begin
                        flag[i]:=true;
                        inc(a[tot].cnt);
                        a[tot].val:=min(a[tot].val,t[i]);
                        find(m[i]);
                    end;
            end;
    end;
    
    procedure qsort(l,r:longint);  inline;
    var i,j,x,y:longint;
    begin
        i:=l; j:=r; x:=m[(l+r)>>1];
        repeat
            while m[i]<x do inc(i);
            while m[j]>x do dec(j);
            if i<=j then
                begin
                    y:=m[i];
                    m[i]:=m[j];
                    m[j]:=y;
                    inc(i);
                    dec(j);
                end;
        until i>j;
        if i<r then qsort(i,r);
        if l<j then qsort(l,j);
    end;
    
    procedure main;
    var i:longint;
    begin
        read(n);
        minvalue:=maxlongint;
        for i:=0 to n-1 do
            begin
                read(m[i]);
                inc(sum,m[i]);
                t[i]:=m[i];
                minvalue:=min(minvalue,m[i]);
            end;
        qsort(0,n-1);
        tot:=0;
        for i:=0 to n-1 do
            begin
                if flag[i] then continue;
                a[tot].val:=t[i];
                a[tot].cnt:=1;
                flag[i]:=true;
                find(m[i]);
                inc(tot);
            end;
        for i:=0 to tot-1 do
            inc(sum,min(a[i].val*(a[i].cnt-2),minvalue*(a[i].cnt+1)+a[i].val));
        writeln(sum);
    end;
    
    begin
        main;
    end.
    View Code

      题目:http://poj.org/problem?id=2369

      题意:给出1-n的一个排列,a1,a2,...,an,表示P(1)=a1,P(2)=a2,...,P(n)=an,P(P(1))=P(a1),P(P(2))=P(a2),

      ...,P(P(n))=P(an).问经过多少次后使得P(1)=1,...,P(n)=n.

      这个是置换群的概念题,找到每个循环节,确定其长度len1,len2,...,lenk,求他们的最小公倍数。
      对于题目中给定的一个例子进行分析:
      1 2 3 4 5
      4 1 5 2 3
      上面定义了函数P,那么我们可以看出这个置换可以写成(1 4 2)(3 5),这样循环1中的数绝对不会跑到第二个循环中,每个循环i需要经过leni次后就可以到达目的状态,所以我们只需要确定各个循环节长度的最小公倍数。

      
    const maxn=1001;
    
    var val,v:array [0..maxn] of longint;
        n:longint;
        
    function gcd(x,y:int64):longint;
    begin
        if x mod y=0 then exit(y)
        else exit(gcd(y,x mod y));
    end;
    
    procedure main;
    var i,j,k,sum:longint;
        ans:int64;
    begin
        read(n);
        ans:=1;
        for i:=1 to n do 
        read(val[i]);
        for i:=1 to n do 
            if (v[i]=0) and (val[i]<>i) then 
                begin
                    v[val[i]]:=1;
                    j:=val[i];
                    k:=2;
                    while val[j]<>i do 
                        begin
                            inc(k);
                            v[val[j]]:=1;
                            j:=val[j];
                        end;
                    v[i]:=1;
                    sum:=gcd(ans,k);
                    ans:=ans*(k div sum);
                end;
        writeln(ans);
    end;
    
    begin
        main;
    end.
    View Code

      题目:http://poj.org/problem?id=1026

      题意:首先给出一个置换,然后给出一个字符串,问置换k次之后得到的字符串是什么?

      我们求出来子循环,然后对每个子循环计算k次之后置换群变成什么排列,用b[0],b[1],...,b[t-1]表示一个子群,

      那么长度为t,经过一次置换后变成b[0]=b[1],b[1]=b[2],..,b[t-1]=b[0],所以经过k次后变成b[(0+k)%t],b[(1+k)%t],..,b[(t-1+k)%t],即b[i]->b[(i+k)%t].

      我们预处理出2^k,然后乱搞一下即可

      
    const maxn=201;
        maxm=31;
    
    type arr=array [0..maxn] of longint;
    
    var    n,i,j,k,l:longint;
        cache,ans:arr;
        change:array [0..maxm] of arr;
        s:string;
        temp:char;
        
    procedure prepare;
    var i,j:longint;
    begin
        for j:=1 to maxm do 
            for i:=1 to n do 
                change[j,i]:=change[j-1,change[j-1,i]];
    end;
        
    procedure main;
    begin
        repeat
            read(n);
            if n=0 then break;
            for i:=1 to n do
                read(change[0,i]);
            readln;
            prepare;
                repeat
                    read(k);
                    if k=0 then break;
                    read(temp);
                    readln(s);
                    l:=length(s);
                    while l<n do
                        begin
                            s:=s+' ';
                            inc(l);
                        end;
                    for i:=1 to n do
                        ans[i]:=i;
                    while k<>0 do
                        begin
                            j:=trunc(ln(k)/ln(2));
                            for i:=1 to n do
                                cache[change[j,i]]:=ans[i];
                            ans:=cache;
                            k:=k-(1<<j);
                        end;
                    for i:=1 to n do
                        write(s[ans[i]]);
                writeln;
            until false;
            writeln;
        until false;
    end;
    
    begin
        main;
    end.
    View Code

    再说下循环的概念。

    记(a1 a2 ^ an)= 为一个循环。循环亦称做轮换。可以认为是a1到an组成了一个环。而一个置换可以写成多个循环的乘积。比如

    =(a1a3a6)(a2a4)(a5)。而循环节的长度就是轮换的个数。这里循环节长度为3。

    对于循环有一些操作。比如乘上一个[对换]。

    定义(a1,b1)为将a循环中的a1元素和b循环中的b1元素交换。则这是一个两元素在不同轮换中的对换。给循环乘上这个对换。即相当于将原来的两个“环”分别在a1和b1处拆开,再连接成一个新的“环”。也就是说,就是这种对换将两个轮换合并成了一个。

    反之,如果对换发生在某轮换内部,那么相当于在(a1,ai)处将此环拆开,然后分别合并为了两个新“环”。也就是说,这种对换将轮换分拆为了两个新的轮换。

    如果我们记置换群中元素个数为n,循环节长度为a,可以发生的内部对换数为b。则有下列式子成立:

    b = n-a。

    想到了神马?对了,最小路径覆盖。

    实际上,二分图就是一个置换群。上面一排元素为X集,下面一排元素为Y集。在利用二分图求解最小路径覆盖问题的时候,每次增加一个匹配,路径数就会减少一条。也就是说,匹配数+路径条数=顶点个数。如果想要尽量减小路径条数,大家都看得出来要求最大匹配。

    类比一下,匹配数即为轮换内对换数,路径条数即为循环节长度,而顶点个数也就是置换群内的元素个数。所以说,此题其实是求置换群中循环节的长度。

    还有一个记忆犹新的例子。那就是某个神奇的DP题。

    题目大意大概就是一些钥匙分别对应一些门,但是这些钥匙分别放在不同的门里,并且锁起来了(多么悲催~~)。现在你有两条途径得到这些钥匙。要么破坏门,拿出里面的钥匙。要么用之前得到的钥匙去开现在面对的门。求最少破坏的门的数目。

    看出来了,对不对?门看做大括号上面一排元素,钥匙看做下面一排。如果出现“钥匙转圈”的现象,那么即形成一个轮换。此题即变为求循环节的长度。

    而用dp求解的时候,我们写方程如下:

    f[i,j]:=min{f[i-1,j-1],(i-1)*f[i-1,j]}。这样,第一个式子是设前面i-1个元素组成一个循环,而后一个式子则是通过给前i-1个元素组成的循环乘上一个两元素分属于不同轮换的对换将它和第i个元素的轮换合并为了一个。

    这样,不论是DP还是图论,我们都可以统一地用离散数学的群论来总结和解释。

    其实,之前在学习群论的过程中,就隐约体会到了其对于其他领域问题的一些本质解释。比如拓扑排序,比如线性代数。

  • 相关阅读:
    《第一行代码》阅读笔记(十四)——ViewPager
    《第一行代码》阅读笔记(十三)——碎片的最佳案例
    《第一行代码》阅读笔记(十二)——探究碎片
    《第一行代码》阅读笔记(十一)——编写界面的最佳案例
    《第一行代码》阅读笔记(十)——RecyclerView
    《第一行代码》阅读笔记(九)——ListView
    《第一行代码》阅读笔记(八)——自定义控件
    《第一行代码》阅读笔记(七)——Android经典四种布局
    《第一行代码》阅读笔记(六)——AndroidUI控件(初级)
    后台管理系统左侧菜单栏显示隐藏
  • 原文地址:https://www.cnblogs.com/logichandsome/p/4074719.html
Copyright © 2020-2023  润新知