hdu 6058 Kanade's sum 维护链表

本文提出一种高效算法,针对给定数组求所有子序列中第k大的元素之和,通过维护动态链表确保O(nk)的时间复杂度,避免了传统O(n^3 log n)暴力解法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

题意:
给你一个关于1~n的排列,让你求这个排列的所有子序列中第k大的数的和

思路:

最简单的暴力枚举区间,时间复杂度为o(n^3*logn)肯定是不可取的
还有一种暴力的想法是枚举每个数,然后找他在那些区间中是第k大的数(对最后结果有贡献),直接敲了了个o(n^2*80)的算法,肯定也是tle,在这个算法中主要是找前边比他大的k个数和后边k个比他大的数的时候耗时(o(n)的算法),但是我们发现我们只是找比他大的数的下表就能确定他所在区间的个数,那么我们只需要维护一个链表就可以了,这样的话可以瞬间将o(n)的算法变为o(k)(毕竟k最大也就80)
介绍几个数组
pre【i】数组:存的是i之前比他大的数的下标(维护的链表中的前一个元素)
np【i】数组:存的是i之后比他大的数的下标(维护的链表中的后一个元素)
s【i】数组:存的是之前第i个比他大的数的下标
z【i】:存的是之后第i个比他大的数的下标


我们从小到大枚举1----n-k+1之间的数,当我们计算完1个时需要把他从链表中删除,这样就可以保证每次枚举一个数时,他的左右全是比他大的数(比他小的数其实存在下标差中,并不是真的删掉,只是我们找的时候不需要找比他小的数)


举个例子:
         2            1            3          5            4


下标:   1            2            3          4            5




枚举1的时候,1两侧全是比他大的数,然后把1删除变为




         2            3            5          4


下标:   1            3            4          5


我们在枚举2时,2两侧还是只有比他大的数,但是当我们计算个数的时候进行下标运算,被删掉的1也会别计算在里边的
  最后我们要分情况计算个数(前边有x个比他大,后边有k-1-x个比他的)

代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=510000;
int a[N],pos[N];
LL ans;
int pre[N],np[N];
int s[N],s0;
int z[N],z0;
int n,k;
void erase(int x)
{
    int pp=pre[x];
    int nn=np[x];
    if(pre[x])
    np[pre[x]]=nn;
    if(np[x]<=n)
    pre[np[x]]=pp;
    pre[x]=np[x]=0;
}
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        scanf("%d%d",&n,&k);
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
            pos[a[i]]=i;
        }
        for(int i=1;i<=n;i++)
        pre[i]=i-1,np[i]=i+1;
        ans=0;
        for(int i=1;i<=n-k+1;i++)//从小到大枚举1~n-k+1中的数
        {
            int p=pos[i];
            s0=0;
            z0=0;
            for(int d=p;d&&s0<=k+1;d=pre[d])//往前找k个比他大的数,并记录他的下标
            s[++s0]=d;
            for(int d=p;d!=n+1&&z0<=k+1;d=np[d])//往后找k个比他大的数,并记录他的下标
            z[++z0]=d;
            s[++s0]=0;//前边比他大的数<k的时候,方便计算排头比他小的数的个数
            z[++z0]=n+1;//后边比他大的数<k的时候,方便计算末尾比他小的数的个数
            for(int j=1;j<=s0-1;j++)//枚举情况数前边有j-1个后边有k-(j+1)个
            {
                if(k+1-j<=z0-1&&k+1-j>=1)
                ans+=(z[k+1-j+1]-z[k+1-j])*1LL*(s[j]-s[j+1])*i;
            }
            erase(p);
        }
        cout<<ans<<endl;
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值