AT_s8pc_5_f Lunch Menu 题解
AT_s8pc_5_f Lunch Menu 题解
知识点
区间最值背包 DP ,模拟退火(假)。
题意简述
给定 \(N,M,Q\),以及长度为 \(N\) 的数组 \(\{ a_i \}_1^{N}\),\(Q\) 个区间 \(\{ [l_i,r_i] \}_1^Q\)。
现可以将至多 \(M\) 个 \(a_i\) 变为 \(0\),问 \(\sum_{i=1}^Q (\max_{j=l_i}^{r_i} a_j)\) 的最小值。
分析
对于单个区间,为了让最大值变小,肯定首先从区间中的最大值开始变为 \(0\),扩展到全局也是如此。那么就有一个思路:从最大值开始降序一个个决策是否删除。
假设一开始有一个区间集合 \(S\),包含题目给定的 \(Q\) 个区间。现在枚举到 \(i\)。
- 不删除:把 \(S\) 中跨越 \(i\) 的区间贡献设为它,并加入答案,然后从 \(S\) 中删除。
- 删除:直接跳过,然后枚举下一个。
但是这样说很模糊,因为除了爆搜很难提出具体的方案。我们考虑如何让从区间集合中找到和删除跨越它的区间,难道直接压缩然后记在状态里吗?
并不用。把跨越某个点的区间删除之后,这个点就没有任何用处了,我们可以直接把它删掉,然后把 \(S\) 分成两个新的 \(S’_l,S'_r\),分别放到左边和右边。就像在笛卡尔树上做分治一样,但是我们这里与笛卡尔树有两个区别:
- \(a_i\) 数值一样的不好处理。
- 决策时删除对应的跳过不好处理。
那又该怎么办呢?注意到数据范围很小,我们可以直接记下当前处理的区间 \([L,R]\),然后就可以做区间 DP。为了方便理解,我们类比笛卡尔树上分治,称其为“分治区间”(不知道笛卡尔树也没关系,这只是个类比),其实就是 \(S\) 变成一个包含并且只包含所有满足 \(L\le l_i \land r_i \le R\) 的区间的集合,这也是我们选用区间 DP 的原因。
接下来回到决策方面:
- 不删除:把 \(S\) 中跨越 \(i\) 的区间贡献设为它,并加入答案,然后从 \(S\) 中删除。对应的是:从分治区间 \([L,R]\) 下到 \([L,i-1],[i+1,R]\)。
- 删除:直接跳过,然后枚举下一个。
然后加上显然的枚举到第几个和还剩几次赋值为 \(0\) 的机会两个参数,就可以 DP 了。
设 \(f_{i,j,l,r}\) 表示现在枚举到倒数第 \(i\) 大的数,还剩 \(j\) 次赋值为 \(0\) 的机会,当前分治区间为 \([l,r]\) 的最小和;\(cnt_{l,r,mid}\) 表示现在分治区间为 \([l,r]\) 的情况下,区间集合 \(S\) 内有几个区间跨域点 \(mid\);\(pos_i,w_i\) 表示倒数第 \(i\) 大的数的位置和值。
转移方程:(\(a \gets b\) 表示 tomin(a,b),即 \(a = \min{(a,b)}\))
-
不删除:
\[f_{i,j,l,r} \gets \min_{k=0}^j {\{f_{i-1,j-k,l,i-1}[l < i] + f_{i-1,k,i+1,r}[i < r] + cnt_{l,r,pos_i} \times w_i \}} \\ \] -
删除:
\[f_{i,j,l,r} \gets f_{i,j-1,l,r} \\ \]
为了方便理解和编码,我们可以选用记忆化搜索来进行 DP。
代码
时间复杂度:\(O(N^3M^2+N^3Q)\),空间复杂度:\(O(N^3M)\)。
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define LINF 0x3f3f3f3f3f3f3f3f
#define ll long long
#define RCL(a,b,c,d) memset(a,b,sizeof(c)*(d))
#define tomin(a,...) ((a)=min({(a),__VA_ARGS__}))
#define tomax(a,...) ((a)=max({(a),__VA_ARGS__}))
#define FOR(i,a,b) for(int i(a); i<=(int)(b); ++i)
#define DOR(i,a,b) for(int i(a); i>=(int)(b); --i)
#define EDGE(g,i,x,y) for(int i(g.h[x]),y(g[i].v); ~i; y=g[i=g[i].nxt].v)
using namespace std;
constexpr int N(50+10);
namespace IOEstream {
//...快读快写省略
} using namespace IOEstream;
int n,m,Q;
int a[N],l[N],r[N];
int cnt[N][N][N];
ll f[N][N][N][N];
struct node {
int idx,val;
friend bool operator <(node a,node b) {
return a.val<b.val;
}
} b[N];
ll dp(int u,int rem,int l,int r) {
if(~f[u][rem][l][r])return f[u][rem][l][r];
ll &res(f[u][rem][l][r]);
res=LINF;
if(!u||l>r)return res=0;
if(b[u].idx<l||b[u].idx>r)return res=dp(u-1,rem,l,r);
if(rem)res=dp(u-1,rem-1,l,r);
FOR(i,0,rem)
tomin(res,dp(u-1,i,l,b[u].idx-1)+dp(u-1,rem-i,b[u].idx+1,r)+(ll)cnt[l][r][b[u].idx]*b[u].val);
return res;
}
int main() {
I(n,m,Q);
FOR(i,1,n)I(a[i]);
FOR(i,1,Q)I(l[i],r[i]);
FOR(i,1,n)b[i]= {i,a[i]};
sort(b+1,b+n+1),RCL(f,-1,f,1);
FOR(L,1,n)FOR(R,L,n)FOR(i,1,Q)if(L<=l[i]&&r[i]<=R)FOR(mid,l[i],r[i])++cnt[L][R][mid];
O(dp(n,m,1,n),'\n');
return 0;
}
关于模拟退火
为什么我在模拟退火的后面加了一个“假”呢?
因为按照我在某校 OJ 上看到的题解,这东西还要拼一个贪心过部分分才行,所以并不是一个完全的算法,不过拿来骗分也不失为一种好方法。
思路
先把 \(M\) 个赋值为 \(0\) 的数加进去,然后每次随机三个为 \(0\) 的位置和三个不为 \(0\) 的位置交换,套上退火模版。
然后写完了发现可能过不了第三个子任务,这时候我们套个贪心设定一下初始时 \(M\) 个赋值为 \(0\) 的数,然后在退火。
(思路来源于某校 OJ 上看到的题解,没有实现,这里仅仅是提供思路。)

浙公网安备 33010602011771号