题意
有n个数集,每个数集里最多只有10个元素,现在从每个数集里面选数一个数,假设选出的数的和是p,给出k,问前k小的p的和。
n,k<=100000
分析
首先二分答案lim,然后考虑如何找到所有不大于lim的p的和。
设函数solve(num,kth)表示当前状态的和为num,上一次拓展的位置为kth。
用一颗线段树维护每个位置下一个选的数与上一个选的数的差的最小值,然后就在kth到n里面找到所有不大于lim-num的位置,那么这些位置都是可以拓展的。
当我们找到了超过k个数或者找完时就可以退出了。
总的复杂度是 O((n+k)logplogn) O ( ( n + k ) l o g p l o g n )
一开始打的是在每次在线段树的叶节点继续递归solve(),然后就被卡栈了。。。
接着改了改,每次先修改然后回溯完再修改回去,然后就超时了。。。
接下来就是各种卡常,卡了一晚上多终于被我卡了过去啦。
代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;typedef long long LL;const int N=100005;
const LL inf=(LL)1e15;int n,k,sz,pos,x,y,P;
LL sum,lim,a[N][15],val,LIM;
struct tree{LL mn;int id;}t[N*4];int read()
{int x=0,f=1;char ch=getchar();while (ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while (ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;
}void updata(int d)
{while (d>1) d>>=1,t[d].mn=min(t[d<<1].mn,t[d<<1|1].mn);
}void build(int d,int l,int r)
{if (l==r) {t[d].id=1;t[d].mn=a[l][2]-a[l][1];return;}int mid=(l+r)>>1;build(d<<1,l,mid);build(d<<1|1,mid+1,r);t[d].mn=min(t[d<<1].mn,t[d<<1|1].mn);
}inline bool get(int d,int l,int r)
{if (l==r){pos=l;P=d;//int id=++t[d].id;t[d].mn=a[l][id+1]-a[l][id];return 1;}int mid=(l+r)>>1;if (x<=mid&&t[d<<1].mn<=LIM&&get(d<<1,l,mid)) return 1;return y>mid&&t[d<<1|1].mn<=LIM?get(d<<1|1,mid+1,r):0;
}void solve(LL num,int kth)
{sz++;sum+=num;int tmp=kth-1;x=tmp+1;y=n;LIM=lim-num;while (sz<k&&tmp<n&&get(1,1,n)){tmp=pos;int id=t[P].id;LL tmpnum=num,mn=t[P].mn;while (mn<=lim-tmpnum&&sz<k){tmpnum+=mn;id++;mn=a[tmp][id+1]-a[tmp][id];solve(tmpnum,tmp+1);}x=tmp+1;y=n;LIM=lim-num;}
}int main()
{n=read();k=read();LL num=0;for (int i=1;i<=n;i++){int tot=read();for (int j=1;j<=tot;j++) a[i][j]=read();sort(a[i]+1,a[i]+tot+1);num+=a[i][1];a[i][tot+1]=inf;}build(1,1,n);LL l=num,r=(LL)1e13;while (l<=r){lim=(l+r)>>1;sum=0;sz=0;solve(num,1);if (sz==k) r=lim-1;else l=lim+1;}sum=0;sz=0;lim=r;solve(num,1);printf("%lld",sum+(LL)(k-sz)*(r+1));return 0;
}