K-th 问题的一般思路
是在同一个情景下,求出前 \(K\) 类最小的方案价值。
其可以等效转化为:
将每一种方案视作一个状态,并通过状态之间的大小关系连边(严格),我们求出其拓扑序的前 \(k\) 个节点。
笔者认为,所有的优化方案本质上都是在尽可能少的边数下保留这个拓扑结构,亦或者是利用隐式建图等技巧(因为事实上绝大部分情况状态相当多且复杂)
看几个情景:
情景一
给定序列 \(a\),你需要从中选出一个数,求选出数权值前 \(k\) 小的方案。
排序取前 \(k\) 小即可。
我们分别根据其向两个方向的 Expand 来设立情景二与情景三
情景二
从扩展维度入手。
给定两个序列 \(a,b\),定义方案 \((i,j)\) 的权值是 \(a_i+b_j\),求前 \(k\) 小方案。
先将 \(a,b\) 排序,首先最小方案必然是 \((0,0)\),且有 \((i,j)\le \min((i+1,j),(i,j+1))\)
朴素方法
使用小根堆,加入 \((0,0)\),我们每次取出 \((i,j)\),如果已经取出就 continue
,将 \((i+1,j),(i,j+1)\) 加入堆。
前 \(k\) 个堆顶即为所求。
这种方法实现简单,思维量小,但是有其弊端:判重。
这启发我们 钦定唯一前驱
优化
一般会自然的考虑指定 \((i,j)\) 的唯一前驱为:
- \(i=0,j=0:\) 无
- \(j>0:\) \((i,j-1)\)
- \(i>0,j=0:\) \((i-1,j)\)
这样仍然保证了每个状态的后继不超过 \(2\) 个,且解决了判重的问题,更是具有更好的扩展性
但是要保证**不会有非最小状态没有前驱
更高维的情形
给定 \(m\) 个序列 \(a_1\sim a_m\),定义方案 \((p_1,p_2\dots,p_m)\) 的权值是 \(\sum_{i=1}^ma_{i,p_i}\),求其前 \(k\) 小方案。
可能我们会自然地沿用上一种情形的构造方法,从后往前找到第一个非零的 \(p\) 并将其减少 \(1\)。
但是这样存在一个问题,对于一个后面有若干个零的状态而言,其后继状态可能会到达 \(O(m)\) 的量级(零的个数)
是容易爆炸的
考虑规避掉这种情况,也就是告诉我们将 \(1\) 改为零需要注意,那么考虑给若干个零中间选一个变成 \(1\) 这种后继也指定一个偏序关系,那么就有:
不妨钦定若 \(i<j\) ,则 \(a_{i,1}-a_{i,0}\le a_{j,1}-a_{j,0}\),也就是选择前面的零变成 \(1\) 所得到的值更小。
这样可以做一个正向类似于轮廓线DP的事情,在逆向角度看就是将一个 \(1\) 改为零后看前一位,如果前一位非零则前移前一位,否则后移前一位并前移自己。
用形式语言描述就是,不妨设 \(p_{x}\) 为:\(p_x>0,p_{x+1}=p_{x+2}=\dots=p_{m}=0\)
这样我们就保证了任意状态的前驱如果存在那么唯一,且后继只会出现如下情况:
设最后一个非零位为 \(x\)
- \(p_x\) 增加 \(1\)
- 将 \(p_{x+1}\) 改为 \(1\)
- \(p_x=1\),将 \(p_{x+1}\) 改为 \(1\),将 \(p_x\) 改为零
所以每个状态的后继不超过 \(3\) 个,相当优秀了。
不过我们仍然需要研究一下状态的表达,毕竟你不可能真的将状态用 \(\lbrace p_1,p_2\dots p_m\rbrace\) 存储下来。
这是容易的,我们发现我们只需要记录下 \(x,p_x\) 以及方案权值即可。
情景三
从扩展选择方法入手。
给定序列 \(a_0\sim a_{n-1}\),定义一个选择方案 \((p_0,p_2\dots p_{m-1})\) 的权值是 \(\sum a_{p_i}\),求出大小为 \(m\) 的前 \(k\) 个选择方案。
同样先排序。
我们从二进制的角度入手,同样考虑为每个状态指定唯一前驱。
不妨考虑由 \(111\dots 100\dots\) 转移到一个任意方案的步骤,一个合理的想法是我们从最后一个 \(1\) 开始依次向后移动一直转移到这个状态当作初始状态到这个状态的一条转移路径,且这样可以保证前驱唯一。
让我们更加形式化一点,状态 \((p_0\dots p_{m-1})\) 的后继为:
找到第一个 \(p_i\neq i\) 的位置 \(x\)(如果没有就取最后一个位置)说明此刻 \([0,p_{x-1}]\) 尚未移动,也就是说明现在仅有两个决策。
- 继续移动 \(p_x\),这要求 \(p_{x}+1<p_{x+1}\)
- 换个继续动,也就是令 \(p_{x-1}=p_{x-1}+1\)。
同样的考虑简化当前决策的表达,发现我们只需要维护 \(x,p_x,p_{x+1},nowval\) 即可,每次的变换就是:
- \((x,p_x,p_{x+1})\to (x,p_x+1,p_{x+1}),p_x+1<p_{x+1}\)
- \((x,p_x,p_{x+1})\to (x-1,x,p_x)\)
Expand
当 \(m\) 的大小不是定值,而是区间 \([l,r]\),我们该如何做?
很简单,我们插入 \(len\) 个初始状态 \(\forall i\in [l-1,r-1]\cap \mathbb{Z},(i,i,n,a_0+a_1+\dots a_{i})\) 即可。
[CCO2020] Shopping Plans
将情景三所用算法维护每一类物品,然后用情景二所用方法维护答案即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 1050500
const int inf=0x3f3f3f3f3f3f3fll;
int id[N];
struct stu{
int i,pi,pi_1,s;
stu(){
i=pi=pi_1=s=0;
}
stu(int _i,int _pi,int _pi_1,int _s){
i=_i,pi=_pi,pi_1=_pi_1,s=_s;
}
bool operator<(const stu b)const {
return s>b.s;
}
};
int n,m,K;
struct node{
//单个
int l,r,n;
vector<int>c,ans;
priority_queue<stu >q;
void init(){
n=c.size();
if(l>n){while(K--){cout<<"-1\n";}exit(0); }
r=min(r,n);
sort(c.begin(),c.end());
if(!l)ans.push_back(0);
for(int i=0,s=0;i<r;++i){
s+=c[i];
if(i+1>=l)q.emplace(i,i,n,s);
}
}
int get(int k){
while(!q.empty()&&ans.size()<k){
auto [i,pi,pi_1,s]=q.top();q.pop();
ans.push_back(s);
if(pi+1<pi_1)q.emplace(i,pi+1,pi_1,s+c[pi+1]-c[pi]);
if(i&&i<pi)q.emplace(i-1,i,pi,s+c[i]-c[i-1]);
}
if(ans.size()<k)return inf;
return ans[k-1];
}
}a[N];
void init(){
cin>>n>>m>>K;
for(int i=1,tp,w;i<=n;++i){
cin>>tp>>w;
a[tp].c.push_back(w);
}
for(int i=1;i<=m;++i)cin>>a[i].l>>a[i].r,id[i]=i;
for(int i=1;i<=m;++i)a[i].init();
sort(id+1,id+m+1,[&](int x,int y){return a[x].get(2)-a[x].get(1)<a[y].get(2)-a[y].get(1);});
}
void sol(){
priority_queue<stu>q;
int s=0;for(int i=1;i<=m;++i)s+=a[i].get(1);
cout<<s<<"\n";--K;
q.emplace(1,2,0,s+a[id[1]].get(2)-a[id[1]].get(1));
while(!q.empty()&&K){
auto [i,p,sbzxy,s]=q.top();q.pop();
if(s>2e14)break;
cout<<s<<"\n";
q.emplace(i,p+1,0,s+a[id[i]].get(p+1)-a[id[i]].get(p));
if(i!=m){
q.emplace(i+1,2,0,s+a[id[i+1]].get(2)-a[id[i+1]].get(1));
if(p==2)q.emplace(i+1,2,0,s+a[id[i]].get(1)-a[id[i]].get(2)+a[id[i+1]].get(2)-a[id[i+1]].get(1));
}--K;
}
while(K--)cout<<"-1\n";
}
signed main(){
init();sol();
}