HDU 2017 多校联合训练赛3 3003 6058 Kanade's sum 枚举 模拟链表

Kanade's sum

Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)


Problem Description
Give you an array A[1..n] of length n .

Let f(l,r,k) be the k-th largest element of A[l..r] .

Specially , f(l,r,k)=0 if rl+1<k .

Give you k , you need to calculate nl=1nr=lf(l,r,k)

There are T test cases.

1T10

kmin(n,80)

A[1..n] is a permutation of [1..n]

n5105
 

Input
There is only one integer T on first line.

For each test case,there are only two integers n , k on first line,and the second line consists of n integers which means the array A[1..n]
 

Output
For each test case,output an integer, which means the answer.
 

Sample Input
  
  
1 5 2 1 2 3 4 5
 

Sample Output
  
  
30
 

Source
2017 Multi-University Training Contest - Team 3


题目大意
给出一个长度为n的数列,要求所有子区间的第k大的数的和。

题目分析
首先想到,暴力遍历所有的子区间,找第k大的数,显然超时。那么我们能不能从数列元素出发,假设每个元素是第k大的数,然后寻找有多少区间能满足该数作为第k大的数的条件。其实,限制一个数能不能作为第k大的数的条件,只是数列中比这个数大的数。那么我们可以找到在被枚举到的数的左右两边比他大的数的位置。如图:

图中,五角星代表我们假设的第k大的数(在图中是第3大的数),三角形代表我们找到的比五角星大的数(最多找到3个即可,即k个,至于为什么是三个而不是两个,后面解释),圆形代表比五角星小的数,或者没有比较到的数(注意,图中标号代表数列中每个数的位置,不代表数的大小)。现在,我们假设五角星左边比他大的两个数与它组成区间,即区间[5, 9](两条实线之间的部分),在这个区间里五角星是第k大的数,在这个区间的基础上,我们还可以从左边的区间tp1中选择 2或3或4 三个位置作为区间的左端点,或者从tp2中选择10作为右端点,这样,左端点我们有4种选择,右端点我们有2种选择,根据排列组合,一共有4*2=8种区间符合要求。另外,满足五角星作为第三大的数的要求,还可以选择两个右边比五角星大的数11,14,或者选择左右两边各一个比五角星大的数,组成连续区间,方法与上述流程相同。至于前边的问题,左右两边,为什么每边要找到三个而不是两个比五角星大的数?因为,如果我们选择左边的两个数与五角星组成第k大的数,我们还需要计算在5位置的左边,还有几个连续的比五角星小的数作为可选择的区间。

至于代码实现,有两种方法,
第一种方法,设置两个数组l[ ],r[ ],分别记录每个枚举到的数的左边和右边比这个数大的数之间的距离,每次枚举一个数作为第k大的数,都要遍历它的左右两边,找到比他大的数,然后计算numl = l[ ]-l[ ]; numr = r[ ]-r[ ]; res += a[i]*numl*numr;(numl,numr是左右两边可以取到的端点的个数)。这种方法,更好理解一点,但是有人ac,有人超时,很遗憾,我的代码是超时的,在下边贴出来,仅供大家参考思路吧。

第二种方法,思想上使用了链表的思想,用数组模拟了一个链表,设置了四个数组,a[maxn], pos[maxn], pre[maxn], np[maxn],a存数列,pos[ i ]存i在数列中的位置,pre存前驱的位置,np存后继的位置。然后从最小的数1出发,假设它是第k大的数,左边k个,右边k个就是我们要找的点,然后就可以计算,(注意,l[ ],r[ ]记录的是数的位置),下一步,就是删除链表中的1所在的节点,这样以2作为第k大的数的时候,它的左右两边的数也都是比他大的数。(注意eraser()函数只是改变了链表的前驱和后继指针,每个节点之间的相对位置是不变的)


方法二 模拟链表 ac代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 500005;
int n, k, T;
ll ans;
int a[maxn];
int pos[maxn], pre[maxn], np[maxn];
int l[maxn], r[maxn];
int cntl, cntr;

void eraser(int x)//删除模拟链表中的x点,注意删节点后,各点之间的相对位置不变
{
    int pp = pre[x];
    int nn = np[x];
    if (pre[x] > 0)
        np[pre[x]] = nn;
    if (np[x] <= n)
        pre[np[x]] = pp;
    pre[x] = np[x] = 0;//有没有无所谓
}

int main()
{
    //freopen("1002.in", "r", stdin);
    //freopen("1002.txt", "w", stdout);
    scanf ("%d",&T);
    while (T--)
    {
        scanf ("%d %d",&n,&k);
        for (int i=1; i<=n; ++i)
        {
            scanf ("%d",&a[i]);
            pos[a[i]] = i;
            pre[i] = i-1;
            np[i] = i+1;
        }
        ans = 0;
        for (int i=1; i<=n; ++i)
        {
            int p = pos[i];
            cntl = cntr = 0;
            for (int j=p; j>0&&cntl<=k; j=pre[j])
                //之所以j>0,是因为pre[1] == 0,数列a向前遍历,要保证不会数组越界
                //再注意,cntl<=k,说明最多需要找到k+1个比a[pos[i]]大的数,而l[1]是a本身,k个比a大的数中数,多出来的那一个数是用来求,当a和它左边的k-1个数组成数列时,它左边可选区间的大小
                l[++cntl] = j;
            for (int j=p; j!=n+1&&cntr<=k; j=np[j])
                r[++cntr] = j;
            l[++cntl] = 0;//如果第cntl个数为数列最左的数,那么可选的区间大小为l[cntl+1]-l[cntl] == 1
            r[++cntr] = n+1;
            for (int j=1; j<=cntl-1; j++)
            {
                if(k+1-j<=cntr && k+1-j>=1)
                    //另一种解释,(cntr-1)+j>=k+1 && k+1-j>=1//保证左边比a[p]大的数最多(==0)时,右边比a[p]大的数仍足够k+1个 && 保证左边比a[p]大的数最多时,仍然在k+1中给a[p]留有位置
                    ans += (r[k+1-j+1]-r[k+1-j])* 1LL *(l[j]-l[i+1]) * i;
            }
            eraser(p);
        }
        printf ("%lld\n",ans);
    }
    return 0;
}


方法一 代码 TLE 仅供参考思路
#include <cstdio>             //TLE
#include <cstring>
using namespace std;
int n, k, T;
int a[500005];
int l[500005], r[500005];//l[]:在a[i]左边,第cntl个比它大的数的位置与a[i]之间的距离
int res;
int i, j;
int cntl, cntr;//cntl:表示a[i]左边有多少个比它大的数

int main()
{
    //freopen("1003.in", "r", stdin);
    //freopen("1003.txt", "w", stdout);

    scanf ("%d",&T);
    while (T--)
    {
        scanf ("%d %d",&n,&k);
        res = 0;
        //memset(a, 0, sizeof(a));
        for (i=1; i<=n; ++i)
            scanf ("%d",&a[i]);
        for (i=1; i<=n; ++i)//枚举,假设a[i]为第k大的数
        {
            cntl = cntr = 1;
            int tmp = a[i];
            for (j=i-1; j>=1; --j)
            {
                if (cntl > k)//找到k个比a[i]大的数
                    break;
                if (a[j] > tmp)
                    l[cntl++] = i-j;
            }
            if (j < 1)//遍历到边界
                l[cntl] = i;//attention

            for (j=i+1; j<=n; ++j)
            {
                if (cntr > k)
                    break;
                if (a[j] > tmp)
                    r[cntr++] = j-i;
            }
            if (j > n)
                r[cntr] = n-i+1;//attention

            //printf ("cnta - %d  cntb - %d\n",cntl,cntr);

            for (int u=cntl-1; u>=0; --u)
            {
                int numl, numr;//左边和右边可用区间长度
                if (cntr-1 < k-1-u)//如果左边和右边的大数都算上(再加上它自己),都不能达到k个数,那么break;注意两边比a[i]大的数都是cnt-1
                    break;
                numl = l[u+1]-l[u];
                numr = r[k-u]-r[k-u-1];
                res += a[i]*numl*numr;
                //printf ("cnta - %d  cntb - %d\n",cntl,cntr);
                //printf ("num - %d  tp1 - %d  tp2 - %d\n",a[i],numl,numr);
                //printf ("---------  %d\n",a[i]*numl*numr);
            }
            /*
            printf ("res  ==  %d\n",res);
            printf ("l  ");
            for(int i=1; i<=n; i++)
            {
                printf ("%d  ",l[i]);
            }
            printf ("\n");
            printf ("r  ");
            for(int i=1; i<=n; i++)
            {
                printf ("%d  ",r[i]);
            }
            printf ("\n");
            */
        }
        printf ("%d\n",res);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值