1. 程式人生 > 實用技巧 >題解:洛谷P5749 [IOI2019]排列鞋子

題解:洛谷P5749 [IOI2019]排列鞋子

題解:P5749 [IOI2019]排列鞋子

約定:下文統一用\(x\)\(-x\)來表示鞋的大小,其中\(x>0\)。且\(-x\)表示左腳鞋,\(x\)表示右腳鞋。

思路:

第一步:從左向右遍歷,對於每一個\(x>0\)的鞋,找到最左邊的可以與它配對的鞋(是\(-x\)且沒有與其他鞋配對),將兩者配對。

(我們暫時只把兩者標記為互相配對,具體計算和交換鞋子等操作在以後的步驟中)

解釋:從左向右配對而每次找最左邊的一定是最優解。如下圖:顯然綠色長度之和小於藍色長度之和。也就是說,上文所述的策略實際上是避免了兩個相距很遠的鞋子互相配對。而且將其推廣到所有情況,都成立。(嚴格數學證明可以參考其他題解)

第二步:再次從左向右遍歷,每遇到一個沒有操作過的鞋(不論左右腳鞋)就將與它配對的鞋換到它邊上。並將兩者標記為“操作過”。(這一步驟開始時,所有鞋都看作未操作過)

解釋:設找到的鞋是\(a\),與\(a\)配對的鞋是\(b\)。因為是從左向右遍歷,所以\(a\)\(b\)相比一定更靠左,且只有\(a\)的右側有 沒有操作過的鞋 。把\(b\)換過來,相當於\(a\)的右側少了一隻鞋,且\(a\)\(b\)不可能在沒有沒有操作鞋中間。使得對後面的交換而言,實際上就可以忽略\(a\)\(b\),而且也縮短了別的未操作的鞋子之間的距離。

第三步:在第二步的基礎上計算移動次數。

程式碼實現:

第一步:使用連結串列(或前向星或 vector )儲存尺碼為\(x\)

的鞋的出現位置。那麼即可實現\(O(1)\)查詢到當前最左邊的可配對的鞋。
開一個數組\(b[]\)儲存每一隻鞋它的搭檔的位置。如\(a\)\(c\)\(a\)\(c\)為下標)配對,則\(b[a]=c;b[c]=a\)
按照思路中說的遍歷一遍,處理好\(b\)陣列即可。

第二步:(將思路中的第二步與第三步結合):參考思路中的第二步。顯然不能真的去模擬交換的過程(顯然超時)。將兩隻鞋移到一起的交換次數就等於兩者下標之差。但是,根據上文可知,兩隻鞋之間的距離會因為先於它們的鞋的交換而改變,而且可以知道現在的兩隻鞋中間每有一隻已經標記過的鞋,兩者之間的距離就會少 \(1\)
使用樹狀陣列維護未標記的鞋的字首和,如果某隻鞋(還有它的搭檔)已經被標記,那麼就樹狀陣列單點修改即可。同時,樹狀陣列可以一邊遍歷鞋,一邊統計答案。

具體細節看程式碼,時間複雜度在 $ O(n \ log_2 \ n)$ 左右。:

#include<bits/stdc++.h>
using namespace std;
const int e=(1e5)*4+10;
int n,a[e];
int cnt=0,head[e];//
struct nod//前向星 可以把它看成連結串列
{
    //可以通過head陣列加上for迴圈來遍歷每一個
    int next,pl;// pl 指的是當前訪問到的鞋在原來數列中的下標,next則相當於指向下一個的指標
}s[e];
int b[e];//記錄每一隻鞋的對應鞋的下標
long long ans=0,tr[e];
//tr陣列就是樹狀陣列維護的部分
bool vis[e];//在第二步中儲存 一隻鞋是否操作過 的陣列
void add(int x,int ii)
{
    cnt++;
    s[cnt].next=head[x];
    s[cnt].pl=ii;
    head[x]=cnt;
}
int lowbit(int x)
{
    return x&(-x);
}
void xg(int x,int va)//樹狀陣列單點修改
{
    for(int i=x;i<=n;i+=lowbit(i))
    {
        tr[i]+=va;
    }
}
long long sum(int x)//樹狀陣列區間求和
{
    long long res=0;
    for(int i=x;i>0;i-=lowbit(i))
    {
        res+=tr[i];
    }
    return res;
}
int main()
{
 	cin>>n;
 	n=n*2;//一共有 n 雙 鞋。
     ans=0;
 	for(int i=1;i<=n;i++)
 	{
 		cin>>a[i];
         a[i]+=n;//對負數進行處理,避免陣列越界
	 }
     for(int i=n;i>0;i--)
     {
         add(a[i],i);//由於前向星的特殊性質(後進入的先訪問),為了下面方便的從左向右遍歷,需要從後向前(倒序)儲存
     }
    for(int i=1;i<=n;i++)
    {
        if(a[i]>n)//找到每個正數,找到它對應的負數。
        {
            int u=2*n-a[i];//注意:找到負數對應的值,別寫錯。
            int tmp=s[head[u]].pl;
            b[i]=tmp;b[tmp]=i;
            if(tmp>i)ans++;
            //假如負的鞋子在正的鞋子的右邊,那麼在正的和負的鞋子在交換到一起後還需要再一次交換(因為要求負的在左,正的在右)
            head[u]=s[head[u]].next;//修改最左邊的與 i 能配對的 的鞋的位置,為下一次配對做準備。
        }
    }
    memset(vis,false,sizeof(vis));//全部標記為未操作
    for(int i=1;i<=n;i++)
    xg(i,1);//最開始,全部都沒有操作過
    for(int i=1;i<=n;i++)
    {
        if(vis[i]==true)continue;
        long long tmp=sum(b[i]-1)-sum(i);//注意:當前鞋交換所需的次數是兩者中間的鞋的個數(不包括兩邊當前的鞋)所以字首和的下標要注意。
        //顯然 b[i] 在 i 的後面,因為 i 先被遍歷到
        ans+=tmp;
        vis[i]=true;vis[b[i]]=true;//標記為已操作
        xg(i,-1);xg(b[i],-1);//這兩隻鞋操作過以後,它們就不能在另外的操作中計算了(具體參考上文),所以要減掉。
    }
    cout<<ans;
	return 0;
}