HDU 1394 暴力 或 归并排序 或 线段树

7 篇文章 0 订阅
1 篇文章 0 订阅

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1394

题意:给定n个数,一个合法操作是每次可以将数列的第一个元素放到数列的尾部,然后问你所有可能操作中的逆序对的个数最少是多少。一个逆序对(ai,aj)的定义是数列a中下标 i 小于 j 同时 ai 大于 aj。

思路:看到数据范围只有5000,一开始就往暴力方面想了。然后模拟一下发现了一个比简单的规律。对于一个n个数的数列a,b,c,d……..,每个数都是0到n-1,设当前的逆序对个数是cnt,那么一次操作后逆序对个数会变成cnt+n-2*a-1个,其实蛮显然的,因为数列中比a大的数有n-1-a个,比a小的数有a个。O(n*n)计算出事的逆序对个数,然后在O(n)遍历一边统计最小值就可以了。

暴力
#include<bits/stdc++.h>         
using namespace std;
const int inf = 0x3f3f3f3f;

int main()
{
    int n;
    int a[5003];
    while(~scanf("%d",&n)){
        for(int i=0;i<n;i++){
            scanf("%d",&a[i]);
        }
        int cnt=0;
        for(int i=0;i<n;i++){
            for(int j=i+1;j<n;j++){
                if(a[j]<a[i])   cnt++;
            }
        }
        int mi=inf;
        for(int i=0;i<n;i++){
            cnt+=(n-1-(2*a[i]));
            mi=min(cnt,mi);
        }
        printf("%d\n",mi);
    }
    return 0;
}

这题的数据范围很小,在大数据的情况暴力可能会T。想了一下优化,有两个思路,一个是归并排序优化,一个是线段树优化。


归并排序

考虑这样一个情况,把n个数的序列A分为一般长度的两个序列L和R,那么A的逆序对个数就等于L、R中自身的逆序对个数之和再加上跨越L和R的逆序对个数。这不正好是归并排序的思想么。R和L中的逆序对个数迭代到1的时候很容易计算,那么如何快速计算跨越L和R的逆序对re呢。基于这样一个事实,我们分别把L、R中的数据进行排序是不会影响re的值的。排序后如果L中下标为ll的值比R中下标为rr的值大,那么re就应该加上L的长度n1-ll。(自己想一下为什么)

#include<bits/stdc++.h>
using namespace std;
const int maxn = 50003;
const int inf = 0x3f3f3f3f;
int n;

int solve(int *a,int l,int r)
{
    if(l>=r)
        return 0;
    int re=0;
    int mid=(r+l)>>1;
    int sig=solve(a,l,mid)+solve(a,mid+1,r);
    int n1=mid-l+1;
    int n2=r-mid;
    int L[n1],R[n2];
    for(int i=0;i<n1;i++)
        L[i]=a[i+l];
    for(int i=0;i<n2;i++)
        R[i]=a[i+mid+1];
    L[n1]=R[n2]=inf;
    int ll=0,rr=0;
    for(int i=l;i<=r;i++){
        if(L[ll]>R[rr]){
            re+=n1-ll;
            a[i]=R[rr++];
        }
        else
            a[i]=L[ll++];
    }
    /*for(int i=0;i<n;i++)
        printf("%d ",a[i]);
    printf("\n");*/
    return re+sig;
}

int main()
{
    int a[maxn],b[maxn];
    while(~scanf("%d",&n)){
        for(int i=0;i<n;i++){
            scanf("%d",&a[i]);
            b[i]=a[i];
        }
        int cnt=solve(b,0,n-1);
        int mi=inf;
        for(int i=0;i<n;i++){
            cnt+=(n-1-(2*a[i]));
            mi=min(cnt,mi);
        }
        printf("%d\n",mi);
    }
    return 0;
}

线段树

这个是我看别人的题解才理解线段树的做法的,感觉自己的线段树还得再学学。思路是这样的,线段树的每个节点的值初始化为0,插入时把其对应的叶子结点更新为1,每遇到一个a[i]就查询当前树里面比它大的数有多少个,累加一下就是初始的逆序对个数了。由于是计算逆序对,所以查询区间应该是a[i]到n-1。

#include<bits/stdc++.h>
using namespace std;

const int maxn = 5003;
const int inf = 0x3f3f3f3f;
int sum[maxn << 2];
void PushUp(int rt)
{
    sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}

void build(int l, int r, int rt)
{
    sum[rt]=0;
    if (l == r)
        return;
    int m = (l + r) >> 1;
    build(l, m, rt << 1);
    build(m + 1, r, rt << 1 | 1);
    PushUp(rt);
}

void update(int p,int l, int r, int rt)
{
    if (l == r)
    {
        sum[rt] ++;
        return;
    }
    int m = (l + r) >> 1;
    if (p <= m) update(p, l, m,  rt << 1);
    else update(p, m + 1, r, rt << 1 | 1);
    PushUp(rt);
}

int query(int ll, int rr, int l, int r, int rt)
{
    if (ll <= l && rr >= r) return sum[rt];
    int m = (l + r) >> 1;
    int ret = 0;
    if (ll <= m) ret += query(ll, rr, l, m, rt << 1);
    if (rr > m) ret += query(ll, rr, m + 1, r, rt << 1 | 1);
    return ret;
}

int main()
{
    int n;
    int a[maxn];
    while(~scanf("%d",&n)){
        int cnt=0;
        build(0,n-1,1);
        for(int i=0;i<n;i++){
            scanf("%d",&a[i]);
            cnt+=query(a[i]+1,n-1,0,n-1,1);
            update(a[i],0,n-1,1);
        }
        int mi=inf;
        for(int i=0;i<n;i++){
            cnt+=(n-1-(2*a[i]));
            mi=min(cnt,mi);
        }
        printf("%d\n",mi);
    }
    return 0;
}

看了一下status,线段树在时间和空间上都是最优的,归并排序和线段树时间复杂度都是Onlogn,但是归并排序用到了比较多的中间数组,所以内存占用比较大,可能释放掉会比较好,有兴趣可以试一下。总结一下,对这道水题我自己口胡了这么多其实没啥大用,如果是比赛当然是首选暴力的,连剪枝都不会考虑。但是像这样也能拓展一下思路,顺便巩固一下其他知识,权当学习了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值