【CodeVS】1245 最小的N个和 - Ⅰ - 原题的几种解法

【题意】有两个长度为 N 的序列A B ,在A B 中各任取一个数可以得到N2个和,求这 N2 个和中最小的 N 个。

【数据范围】Ai,Bi109N105

【思路1】30分做法
直接把 N2 个和都算出来,排序,然后输出前N个。
时间复杂度: O(n2logn)
空间复杂度: O(n2)

【思路2】AC做法
求前N小,涉及到单调性,试着排序使得A,B两个序列从小到大。

我们从1到N依次找到前N小的和,那么每次就要从决策中选出一个最小的元素,现在要求每次决策考虑的元素个数尽可能少。

可以按行把所有的 N2 个和分成 N 类:
第1行:A1+B1<A1+B2<A1+Bi<...<A1+Bn
第2行: A2+B1<A2+B2<A2+Bi<...<A2+Bn
......
第i行: Ai+B1<Ai+B2<Ai+Bi<...<Ai+Bn
......
第n行: An+B1<An+B2<An+Bi<...<An+Bn

对于第i行,若 j<k ,则必然先选 Ai+Bj ,后选 Ai+Bk
那么,在决策的时候只用考虑每一行当前未选的最前一个。

即:设l evel[i] 表示当前第i行的 (Ai+Blevel[i]) 在决策中,每次选取s,满足最小化 (Ai+Blevel[i]) ,然后把 (As+Blevel[s]) 最为当前最小,再 inc(level[s])
至于每次的决策,用堆可以做到 O(logn) 完成。

时间复杂度: O(nlogn)
空间复杂度: 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\A12in
1
2
i
n

其中 (i,j) 这个格子表示 Ai+Bj

我们变为了二维的扩展,即在当前状态中选择一个最小的,然后向下、向右扩展决策状态。
这里需要注意的是,有些格子是不用加入待决策状态中的:它的左边或上边还没有决策出来。
这样有一定的优化作用。

时间复杂度: O(nlogn)
空间复杂度: 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\A12in
1
2
i
n

观察到,对于 (i,j) 这个点,比它小的元素至少有 i×j1 个。
由于我们要求前N小的,所以满足要求的点至少要满足 i×j1<n i×jn
这样我们可以把点的个数缩小至

n1+n2+...+ni+...+nn=O(ni=1n1i)=O(nlogn)

时间复杂度: O(nlog2n)
空间复杂度: O(nlogn)

代码:实测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 内求,最后弄出了调和级数,总共的情况数为 O(nlogn)

  5. 对于方法③,在【变式】会提及。

  6. 方法的比较
    方法①,方法②处理范围较小,询问较多的问题;
    方法③处理范围较大,询问较少的问题。

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值