序列中的交换问题

一、逆序对系列问题

题目: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还是图论,我们都可以统一地用离散数学的群论来总结和解释。

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

原文地址:https://www.cnblogs.com/logichandsome/p/4074719.html