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\),分别放到左边和右边。就像在笛卡尔树上做分治一样,但是我们这里与笛卡尔树有两个区别:

  1. \(a_i\) 数值一样的不好处理。
  2. 决策时删除对应的跳过不好处理。

那又该怎么办呢?注意到数据范围很小,我们可以直接记下当前处理的区间 \([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 上看到的题解,没有实现,这里仅仅是提供思路。)


posted @ 2024-11-25 21:14  Add_Catalyst  阅读(12)  评论(0)    收藏  举报