【CodeVS】1245 最小的N個和 - Ⅰ - 原題的幾種解法

y20070316發表於2016-01-15

【題意】有兩個長度為N

N
的序列A
A
B
B
,在A
A
B
B
中各任取一個數可以得到N2
N^2
個和,求這N2
N^2
個和中最小的N
N
個。

【資料範圍】Ai,Bi109N105

A_i,B_i\le 10^9,N\le 10^5

【思路1】30分做法
直接把N2

N^2
個和都算出來,排序,然後輸出前N個。
時間複雜度:O(n2logn)
O(n^2 \log n)

空間複雜度:O(n2)
O(n^2)

【思路2】AC做法
求前N小,涉及到單調性,試著排序使得A,B兩個序列從小到大。

我們從1到N依次找到前N小的和,那麼每次就要從決策中選出一個最小的元素,現在要求每次決策考慮的元素個數儘可能少。

可以按行把所有的N2

N^2
個和分成N
N
類:
第1行:A1+B1<A1+B2<A1+Bi<...<A1+Bn
A_1+B_1<A_1+B_2<A_1+B_i<...<A_1+B_n

第2行:A2+B1<A2+B2<A2+Bi<...<A2+Bn
A_2+B_1<A_2+B_2<A_2+B_i<...<A_2+B_n

......
......

第i行:Ai+B1<Ai+B2<Ai+Bi<...<Ai+Bn
A_i+B_1<A_i+B_2<A_i+B_i<...<A_i+B_n

......
......

第n行:An+B1<An+B2<An+Bi<...<An+Bn
A_n+B_1<A_n+B_2<A_n+B_i<...<A_n+B_n

對於第i行,若j<k

j<k
,則必然先選Ai+Bj
A_i+B_j
,後選Ai+Bk
A_i+B_k

那麼,在決策的時候只用考慮每一行當前未選的最前一個。

即:設level[i]

evel[i]
表示當前第i行的(Ai+Blevel[i])
(A_i+B_{level[i]})
在決策中,每次選取s,滿足最小化(Ai+Blevel[i])
(A_i+B_{level[i]})
,然後把(As+Blevel[s])
(A_s+B_{level[s]})
最為當前最小,再inc(level[s])
inc(level[s])

至於每次的決策,用堆可以做到O(logn)
O(\log n)
完成。

時間複雜度:O(nlogn)

O(n \log n)

空間複雜度:O(n)
O(n)

程式碼:實測136ms

#include <cstdio>
#include <cctype>
#include <queue>
#include <algorithm>
using namespace std;

const int N=161240;

int n;
int A[N],B[N];
int lev[N];

inline int Read(void)
{
    int s=0,f=1; char c=getchar();
    for (;!isdigit(c);c=getchar()) if (c=='-') f=-1;
    for (;isdigit(c);c=getchar()) s=s*10+c-'0';
    return s; 
}

struct cmp
{
    inline int operator () (int i,int j)
    {
        return A[i]+B[lev[i]]>A[j]+B[lev[j]];
    }
};
priority_queue<int,vector<int>,cmp> q;

int main(void)
{
    n=Read();
    for (int i=1;i<=n;i++) A[i]=Read();
    for (int i=1;i<=n;i++) B[i]=Read();
    sort(A+1,A+n+1);
    sort(B+1,B+n+1);
    fill(lev+1,lev+n+1,1);
    for (int i=1;i<=n;i++) q.push(i);
    int now;
    for (int i=1;i<=n;i++)
    {
        now=q.top(),q.pop();
        printf("%d ",A[now]+B[lev[now]++]);
        q.push(now);
    }
    printf("\n");
    return 0;
}

【思路3】AC做法
首先排序,然後還是用【思路2】的思路,從1到N依次求前N小。

注意到【思路2】中我們只使用了橫向的分類,而沒有考慮縱向的分類,這是可以有一些決策狀態排除掉的。
於是我們想到直接把這些狀態當作一個二維的圖來看:

B\A 1 2 i n
1
2
i
n

其中(i,j)

(i,j)
這個格子表示Ai+Bj
A_i+B_j

我們變為了二維的擴充套件,即在當前狀態中選擇一個最小的,然後向下、向右擴充套件決策狀態。
這裡需要注意的是,有些格子是不用加入待決策狀態中的:它的左邊或上邊還沒有決策出來。
這樣有一定的優化作用。

時間複雜度:O(nlogn)

O(n\log n)

空間複雜度:O(n)
O(n)

程式碼:實測88ms

#include <cstdio>
#include <cctype>
#include <queue>
#include <algorithm>
using namespace std;

#define mp(i,j) make_pair(i,j)
#define fs first
#define sc second

typedef pair<int,int> PairInt;
const int N=161240;

int n;
int A[N],B[N];
int cross[N],line[N];

inline int Read(void)
{
    int s=0,f=1; char c=getchar();
    for (;!isdigit(c);c=getchar()) if (c=='-') f=-1;
    for (;isdigit(c);c=getchar()) s=s*10+c-'0';
    return s; 
}

struct cmp
{
    inline int operator () (PairInt Pi,PairInt Pj)
    {
        return A[Pi.fs]+B[Pi.sc]>A[Pj.fs]+B[Pj.sc];
    }
};
priority_queue<PairInt,vector<PairInt>,cmp> q;

inline int Check(int i,int j)
{
    if (cross[i]+1<j) return 0;
    if (line[j]+1<i) return 0;
    return 1;
}

int main(void)
{
    n=Read();
    for (int i=1;i<=n;i++) A[i]=Read();
    for (int i=1;i<=n;i++) B[i]=Read();
    sort(A+1,A+n+1);
    sort(B+1,B+n+1);
    PairInt now;
    q.push(mp(1,1));
    for (int i=1;i<=n;i++)
    {
        now=q.top(),q.pop();
        printf("%d ",A[now.fs]+B[now.sc]);
        cross[now.fs]=now.sc;
        line[now.sc]=now.fs;
        if (Check(now.fs+1,now.sc)) q.push(mp(now.fs+1,now.sc));
        if (Check(now.fs,now.sc+1)) q.push(mp(now.fs,now.sc+1));
    }
    printf("\n");
    return 0;
}

【思路4】AC做法
按照【思路1】,我們把所有可能的情況都算出來,然後排序。
然而【思路1】的時間和空間都是我們承受不了的,怎麼辦?
想辦法把一些一定不可能的狀態給消除掉。

首先還是給A,B排序,同樣還是這個表:

B\A 1 2 i n
1
2
i
n

觀察到,對於(i,j)

(i,j)
這個點,比它小的元素至少有i×j1
i\times j-1
個。
由於我們要求前N小的,所以滿足要求的點至少要滿足i×j1<n
i\times j-1<n
i×jn
i\times j\leq n

這樣我們可以把點的個數縮小至

n1+n2+...+ni+...+nn=O(ni=1n1i)=O(nlogn)
\lfloor {n\over1}\rfloor+\lfloor {n\over2}\rfloor+...+\lfloor {n\over i}\rfloor+...+\lfloor {n\over n}\rfloor=O(n\sum_{i=1}^n{1\over i})=O(n \log n)

時間複雜度:O(nlog2n)

O(n \log^2n)

空間複雜度:O(nlogn)
O(n \log n)

程式碼:實測172ms

#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;

const int N=161240;
const int S=3000000;

int n;
int A[N],B[N];
int t[S],len;

inline int Read(void)
{
    int s=0,f=1; char c=getchar();
    for (;!isdigit(c);c=getchar()) if (c=='-') f=-1;
    for (;isdigit(c);c=getchar()) s=s*10+c-'0';
    return s; 
}

int main(void)
{
    n=Read();
    for (int i=1;i<=n;i++) A[i]=Read();
    for (int i=1;i<=n;i++) B[i]=Read();
    sort(A+1,A+n+1);
    sort(B+1,B+n+1);
    for (int i=1;i<=n;i++)
        for (int j=1;i*j<=n;j++) t[++len]=A[i]+B[j];
    sort(t+1,t+len+1);
    for (int i=1;i<=n;i++) printf("%d ",t[i]);
    printf("\n");
    return 0;
}

【小結】

  1. 對於無序的集合,通常要將它定序,常見的定序方法就是從小到大或者從大到小。

  2. 求第K小的方法通常有以下幾種:
    ①依次求
    ②排序可能情況
    ③二分答案

  3. 對於方法①“依次求”,每次在待定狀態內的元素要儘可能少,可以通過某些性質來減少元素的個數。
    通常的做法是構建多條元素的單調序列,滿足先選完前一個再選後一個,這樣用優先佇列甚至不用(例如《醜數》一題)即可。
    這種做法甚至可以擴充套件到二維單調性,然後在平面上擴充套件。

  4. 對於方法②“排序可能情況”,待定情況要儘可能的少,這要通過某些性質來排除一些不可能的情況。
    例如本題只限定在i×jn

    i\times j\leq n
    內求,最後弄出了調和級數,總共的情況數為O(nlogn)
    O(n log n)

  5. 對於方法③,在【變式】會提及。

  6. 方法的比較
    方法①,方法②處理範圍較小,詢問較多的問題;
    方法③處理範圍較大,詢問較少的問題。

相關文章