1. 程式人生 > 實用技巧 >BZOJ-3032 七夕祭(字首和+中位數)

BZOJ-3032 七夕祭(字首和+中位數)

題目描述

  有 \(N\times M\) 個點,其中 \(T\) 個點是關鍵點,第 \(i\) 個關鍵點座標為 \((x_i,y_i)\),每次可以交換相鄰兩個點,每一行或每一列的第一個位置和最後一個位置也算作相鄰。

  目標是通過交換一些相鄰點,使得每行每列的關鍵點個數相同。如果能滿足全部兩個要求,輸出 both;如果只能使每行關鍵點數量相同,輸出 row;如果只能使每列關鍵點數量相同,輸出 column;如果均不能滿足,輸出 impossible;如果輸出的字串不是 impossible,再輸出最小交換次數。

  資料範圍:\(1\leq N,M\leq 10^5,0\leq T\leq \min(N\times M,10^5)\)

分析

  可以發現,交換左右兩個點只會改變某兩列的關鍵點數,不會改變每行中的關鍵點數。同理,交換上下兩個相鄰的點只會改變某兩行中的關鍵點數,不會改變每列中的關鍵點數。

  所以我們可以分成兩個相互獨立的部分計算:

  \(1.\) 通過最少次數的左右交換使每列中關鍵點數相同。

  \(2.\) 通過最少次數的上下交換使每行中關鍵點數相同。

  統計出在初始情況下,每列中關鍵點總數,記為 \(C[1]\) ~ \(C[M]\)。如果 \(M\nmid T\),則不可能達到要求,如果 \(M\mid T\),則目標就是讓每列中有 \(T/M\) 個關鍵點。

  聯想到 均分紙牌 問題,當所有人手中的紙牌總數 \(T\)

能被 \(M\) 整除時有解,在有解時,我們可以先考慮第一個人:

  \(1.\)\(C[1]>T/M\),則第一個人需要給第二個人 \(C[1]-T/M\) 張紙牌,即把 \(C[2]\) 加上 \(C[1]-T/M\)

  \(2.\)\(C[1]<T/M\),則第一個人需要從第二個人手中拿 \(T/M-C[1]\) 張紙牌,即把 \(C[2]\) 減去 \(T/M-C[1]\)

  按照同樣的方法依次考慮第 \(2\) ~ \(M\) 個人。即使在某個時刻有某個 \(C[i]\) 被減為負數也沒有關係,因為接下來 \(C[i]\) 就會從 \(C[i+1]\) 處拿牌,在實際中可以認為 \(C[i]\)

\(C[i+1]\) 處拿牌發生在 \(C[i-1]\)\(C[i]\) 處拿牌之前。按照這種方法,經過計算,達到目標所需要的最少步數就是:

\[\sum_{i=1}^{M}\Big|i\times \frac{T}{M}-S[i]\Big|,G 是 C 的字首和 \]

  其含義是每個字首最初共持有 \(G[i]\) 張紙牌,最終會持有 \(i\times T/M\) 張紙牌,多退少補,會與後邊的人發生二者之差的絕對值張紙牌的交換。

  如果我們設 \(A[i]=C[i]-T/M\),即一開始就讓每個人手中的紙牌數都減掉 \(T/M\),並且最終讓每個人手裡都恰好有 \(0\) 張紙牌,答案顯然不變,就是:

\[\sum_{i=1}^{M}|S[i]|,S是A的字首和 \]

  回到本題,如果不考慮第 \(1\) 列與最後一列也是相鄰的這一條件,那麼剛才提到的本題中的第 \(1\) 個問題與均分紙牌問題是等價的。若問題有解,一定存在一種適當的順序,使得每一步傳遞紙牌的操作可以轉化為交換一對左右相鄰的攤點(兩個點中有一個是關鍵點)。

  若第 \(1\) 列與最後一列相鄰,則問題等價於一個 環形均分紙牌。仔細思考可以發現,一定存在一種最優解的方案,環上某兩個相鄰的人之間沒有發生紙牌交換操作。於是有一種樸素的做法是,列舉這個沒有發生交換的位置,把環斷開看成一行,轉化為一般的 均分紙牌 問題進行計算。

  首先,一般的 均分紙牌 問題就相當於在第 \(M\) 個人與第 \(1\) 個人之間把環斷開,此時這 \(M\) 個人寫成一行,其持有的紙牌數、字首和分別是:

\[\begin{aligned}A[1]~~~~S[1]\\A[2]~~~~S[2]\\\cdots~~~~~\cdots~\\A[M]~~~~S[M]\end{aligned} \]

  如果在第 \(k\) 個人之後把環斷開寫成一行,這 \(M\) 個人持有的紙牌數、字首和分別是:

\[\begin{aligned}&A[k+1]\quad S[k+1]-S[k]\\&A[k+2]\quad S[k+2]-S[k]\\&\quad\cdots\quad\quad\quad\quad\quad\cdots\quad\quad\\&A[M]\quad \quad \quad S[M]-S[k]\\&A[1]\quad\quad S[1]+S[M]-S[k]\\&\quad\cdots\quad\quad\quad\quad\quad\cdots\quad\quad\\&A[k]\quad\quad S[k]+S[M]-S[k]\end{aligned} \]

  此處 \(A\) 是減去最終每個人手裡紙牌數 \(T/M\) 之後的陣列,\(A\) 陣列均分之後每個人手裡都有 \(0\) 張紙牌,所以 \(S[M]=0\)。也就是說,從第 \(k\) 個人把環斷開寫成一行,字首和陣列的變化是每個位置都減掉 \(S[k]\)

  根據上面推導的公式,所需最少步數為:

\[\sum_{i=1}^{M}|S[i]-S[k]| \]

  不需要列舉 \(k\),把 \(S\) 從小到大排序,取中位數作為 \(S[k]\) 就是最優解,時間複雜度 \(O(N\log N+M\log M)\)

程式碼

#include<bits/stdc++.h>
using namespace std;
int N,M,T;
long long R[100010],C[100010],S[100010];
long long solve(long long *A,int N)
{
    for(int i=1;i<=N;i++)
    {
        A[i]=A[i]-T/N;
        S[i]=S[i-1]+A[i];
    }
    sort(S+1,S+1+N);
    long long ans=0;
    for(int i=1;i<=N;i++)
        ans=ans+abs(S[i]-S[(N+1)/2]);
    return ans;
}
int main()
{
    cin>>N>>M>>T;
    for(int i=1;i<=T;i++)
    {
        int x,y;
        scanf("%d %d",&x,&y);
        R[x]++,C[y]++;
    }
    if(T%N==0&&T%M==0)
        cout<<"both"<<" "<<solve(R,N)+solve(C,M)<<endl;
    else if(T%N==0)
        cout<<"row"<<" "<<solve(R,N)<<endl;
    else if(T%M==0)
        cout<<"column"<<" "<<solve(C,M)<<endl;
    else
        puts("impossible");
    return 0;
}