P6563-[SBCOI2020]一直在你身旁【dp,单调队列】

正题

题目链接:https://www.luogu.com.cn/problem/P6563


题目大意

长度为\(n\)的序列\(a_i\),现在有一个随机\([1,n]\)的整数,每次你可以花费\(a_i\)询问这个数字是否大于\(i\),求猜出所有数至少要多少花费。

\(T\leq 500,\sum n\leq 7000\)

保证\(a_i\)单调不降


解题思路

考虑区间\(dp\),设\(f_{l,r}\)表示猜出区间\([l,r]\)的最小花费。

最基本的转移就是

\[f_{l,r}=min\{\ max\{f_{l,k},f_{k+1,r}\}+a_k\ \}(\ k\in[l,r)\ ) \]

然后考虑如何优化转移。

因为里面有个\(max\),我们可以对于一个\(l,r\)考虑找到一个最小的\(z\)满足\(f_{l,z}>f_{z+1,r}\)那么\(z\)以后的都是用\(f_{l,z}\),以前的都是用\(f_{z+1,r}\)

这个在右端点固定左端点向左时\(z\)是不升的,所以不用二分带\(log\)

对于取\(f_{l,k}+a_k\)的那一部分,\(a_k\)\(f_{l,z}\)都随着\(k\)增大不降,所以直接取\(f_{l,z}+a_z\)

对于\(f_{k+1,r}+a_k\)的那一部分,\(k\)的限制会不断缩小,所以用一个单调队列维护就可以了。

时间复杂度\(O(\sum n^2)\)


code

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N=7200;
int T,n,a[N];
long long f[N][N];
deque<int> q;
long long calc(int k,int r){
	if(k<1)return 1e18;
	return f[r][k+1]+a[k];
}
signed main()
{
	scanf("%d",&T);
	while(T--){
		scanf("%d",&n);
		for(int i=1;i<=n;i++)scanf("%d",&a[i]);
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
				if(i!=j)f[i][j]=1e18; 
		for(int r=2;r<=n;r++){
			q.clear();q.push_back(r-1);
			for(int l=r-1,z=r-1;l>=1;l--){
				while(z>l&&f[z-1][l]>f[r][z])z--;
				while(!q.empty()&&q.front()>=z)q.pop_front();
				if(!q.empty())f[r][l]=calc(q.front(),r);
				f[r][l]=min(f[r][l],f[z][l]+a[z]);
				if(l==1)continue;
				while(!q.empty()&&calc(q.back(),r)>=calc(l-1,r))q.pop_back();
				q.push_back(l-1);
			}
		}
		printf("%lld\n",f[n][1]);
	}
}
posted @ 2021-07-19 20:37  QuantAsk  阅读(24)  评论(0编辑  收藏  举报