题解 【P6646 [CCO2020] Shopping Plans】
command_block
2021-07-14 16:03:56
**题意** : 商店里有 $n$ 个物品,每个物品有种类与价格。
对于第 $i$ 个种类,需要购买商品的个数在 $[l_i,r_i]$ 之间。
求出前 $k$ 便宜的方案的价钱,或指出不存在。
$n,m,k\leq 2\times 10^5$ ,时限$\texttt{2s}$。
------------
- ### Part0
先介绍解决这类“前 $k$ 优”问题的一个通用思路。
我们考虑方案与方案之间的转移。
对于方案 $S$ ,记 ${\rm trans}(S)$ 为 $S$ 的后继方案集合(具体定义暂不明确)。
我们按以下流程求解问题。
- 将花费最小的方案加入小根堆中
- 重复执行直到堆为空,或已得到所有答案
- 将堆顶方案 $S$ 输出并弹出
- 将 ${\rm trans}(S)$ 中的各个方案加入堆中
考虑怎样设计 ${\rm trans}(S)$ 才能使得这个算法正确。
将 $S$ 向 ${\rm trans}(S)$ 连边,形成一张有向图。
- 所有方案形成一棵以花费最小的方案为根的外向树。
这保证了不重不漏。
- ${\rm trans}(S)$ 的权值都大于等于 $S$ 的权值。
这样,不难证明前 $k$ 优的方案构成一个包含根的联通块。我们前述的算法可以找出这个联通块。
- ### Part1 : $m=1,l=r$
将物品按照价格从小到大排序,花费最小的方案是选择最左的 $l$ 个物品。
我们按如下的方法构造 $\rm trans$ :
- 将最左的能右移的物品右移若干,但不能跨越其他已选的物品。
不难发现,这样调整之后权值必然变大,且得到所有方案的方法数恰都为 $1$。符合我们的要求。
> 如图是一个方案对应的调整方法。
> ![](https://cdn.luogu.com.cn/upload/image_hosting/tglfskrf.png)
> 观察 : 在任意时刻,一定是前面有一个前缀是紧密连续的,然后将这个前缀的末端向右移若干。
美中不足的是,单个 $\rm trans(S)$ 集合的大小可能达到 $O(n)$ ,不能保证复杂度。
我们改而让物品每次只能右移一位,并加入当前物品的概念,将 $\rm trans$ 改为 :
- 将当前物品改为紧密前缀的末端(最靠左的能右移的物品)。
- 将当前物品右移一位。
不难发现这两种 $\rm trans$ 是等价的。“当前物品的连续移动”对应前文“将最左的能右移的物品右移若干”。
形式化地,记 $(x,y,z)$ 表示前缀 $[1,x]$ 还没有动,当前物品位于 $y$ ,当前物品右侧的物品位于 $z$ 的方案。
转移 : $(x,y,z)\rightarrow (x,y+1,z)$ 或 $(x-1,x+1,y-1)$。分别对应“移一步”与“ ‘当前物品’前移然后移一步”。
- ### Part1.5 : $m=1$
只需要稍作修正,对于初始未移动的前缀 $(y-1,y,+\infty)$ ,钦定其可以转移到 $(y,y+1,+\infty)$ (多选一个)即可。
- ### Part2 : $m>1,l_i=r_i=1$
最优方案是每个种类选择最小的。
按如下的方法构造 $\rm trans$ :
从前往后考虑种类,记当前种类为 $p$。
- 将种类 $p$ 所选的数右移一位。
- 将 $p$ 后移若干,再将种类 $p$ 所选的数右移一位。
可惜,单个 $\rm trans(S)$ 集合的大小不能保证。
> 原先的 $\rm trans(S)$ 的结构形如图上半部。
> 我们将儿子的权值从小到大排序,然后将儿子串起来(如图下半部),即可将度数变小。
> ![](https://cdn.luogu.com.cn/upload/image_hosting/me3192we.png)
在本题中,我们将种类按照 最小值与次小值的差 从小到大排序,然后如此构造 $\rm trans$ :
- 将种类 $p$ 所选的数右移一位。
- 将 $p$ 后移一位,再将种类 $p$ 所选的数右移一位。
- 若 $p$ 所在种类目前选择次小值,则恢复为最小值。然后将 $p$ 后移一位,再将种类 $p$ 所选的数右移一位。
(我们的排序能保证这一步权值不降)
这一步具体的情形如下图 :
![](https://cdn.luogu.com.cn/upload/image_hosting/x4ew3oy0.png)
- ### Part3 : 一般情况
考虑 **Part1.5** 中的算法,其实就是将 $m=1$ 的问题变成了一个黑箱,支持 $O(\log n)$ 得到下一个最优的解。
观察 **Part2** 中的算法,其实就是维护 $m$ 个黑箱(排好序的数组),操作中只需要访问黑箱的下一个解。
将这两个算法配合起来即可 $O(n\log n)$ 解决本题。
```cpp
#include<algorithm>
#include<cstdio>
#include<vector>
#include<queue>
#define ll long long
#define pb push_back
#define MaxN 200500
using namespace std;
const ll INF=1ll<<60;
struct Data{
int x,y,z;ll sum;
bool operator < (const Data &A) const
{return sum>A.sum;}
};
struct SeqDS
{
vector<ll> ans;
vector<int> a;
priority_queue<Data> q;
int l,r;
void calc(int p)
{
if (p<ans.size())return ;
if (q.empty()){ans.pb(INF);return ;}
Data now=q.top();q.pop();
int x=now.x,y=now.y,z=now.z;ll sum=now.sum;
ans.pb(sum);
if (z==a.size()-1&&x+1==y&&y+1<r)q.push((Data){x+1,y+1,z,sum+a[y+1]});
if (y>=0&&y+1<=z)q.push((Data){x,y+1,z,sum-a[y]+a[y+1]});
if (x>=0&&x+1<y)q.push((Data){x-1,x+1,y-1,sum-a[x]+a[x+1]});
}
void Init()
{
sort(a.begin(),a.end());
if (l>a.size()){ans.pb(INF);ans.pb(INF);return ;}
r=min(r,(int)a.size());
ll sum0=0;
for (int i=0;i<l;i++)sum0+=a[i];
q.push((Data){l-2,l-1,a.size()-1,sum0});
calc(0);calc(1);
}
}T[MaxN];
struct Data2{
int p,j;ll sum;
bool operator < (const Data2 &A) const
{return sum>A.sum;}
};
int tp[MaxN];
bool cmp(int A,int B)
{return T[A].ans[1]-T[A].ans[0]<T[B].ans[1]-T[B].ans[0];}
priority_queue<Data2> q;
int n,m,k;
int main()
{
scanf("%d%d%d",&n,&m,&k);
for (int i=1,c,x;i<=n;i++){
scanf("%d%d",&c,&x);
T[c].a.pb(x);
}
ll sum0=0;
for (int i=1;i<=m;i++){
scanf("%d%d",&T[i].l,&T[i].r);
T[i].Init();tp[i]=i;
sum0=min(INF,sum0+T[i].ans[0]);
}
sort(tp+1,tp+m+1,cmp);
int cnt=0;
q.push((Data2){0,0,sum0});
while(cnt<k&&!q.empty()){
Data2 now=q.top();q.pop();
int p=now.p,j=now.j;ll sum=now.sum;
if (sum>=INF)break;
printf("%lld\n",sum);cnt++;
if (p<m&&j==1)
q.push((Data2){p+1,1,sum-T[tp[p]].ans[1]+T[tp[p]].ans[0]-T[tp[p+1]].ans[0]+T[tp[p+1]].ans[1]});
if (p<m)
q.push((Data2){p+1,1,sum-T[tp[p+1]].ans[0]+T[tp[p+1]].ans[1]});
if (p>=1){
T[tp[p]].calc(j+1);
q.push((Data2){p,j+1,sum-T[tp[p]].ans[j]+T[tp[p]].ans[j+1]});
}
}for (int i=cnt+1;i<=k;i++)puts("-1");
return 0;
}
```