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 r−l+1<k .
Give you k , you need to calculate ∑nl=1∑nr=lf(l,r,k)
There are T test cases.
1≤T≤10
k≤min(n,80)
A[1..n] is a permutation of [1..n]
∑n≤5∗105
Let f(l,r,k) be the k-th largest element of A[l..r] .
Specially , f(l,r,k)=0 if r−l+1<k .
Give you k , you need to calculate ∑nl=1∑nr=lf(l,r,k)
There are T test cases.
1≤T≤10
k≤min(n,80)
A[1..n] is a permutation of [1..n]
∑n≤5∗105
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]
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代码
方法一 代码 TLE 仅供参考思路
题目大意
给出一个长度为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;
}