是在同一個情景下,求出前 \(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();
}