[CF940] Cashback [题解]

\(Cashback\)

题面

有一个长度为 \(n\) 的数列和一个整数 \(c\)

你需要将它分成若干段。

每一段假设长度为 \(k\) ,则我们会去掉前 \(\lfloor \frac{k}{c} \rfloor\) 小的数。

你需要找出一种方案,使得剩下的数的和最小。

分析

ps:此题的贪心思路非常巧妙。

考虑 \(dp\),但很快发现我们不太会转移。

分析一下段长的几种情况,发现我们可以把段分为两大类:

  • 段长小于 \(c\) ,此时我们不会删除任何数,发现它实际上等价于分成若干个恰好长度为 \(1\) 的段。

  • 段长大于 \(c\) ,我们可以分成 \(m\) 个长度为 \(c\) 的段和若干个长度为 \(1\) 的段。

接下来我们对于一些长度为 \(c\) 的段拆开比拼在一起优秀。

其实这不难理解,考虑两个段,拼在一起后最小值和次小值分别为 \(a\)\(b\) ,如果 \(a\)\(b\) 本就分属于两个段,那拆开不劣,如果 \(a\)\(b\) 本来是属于一个段的,拆开肯定是要优秀的。

扩展一下这个情况,发现实际上我们只需要选出一些长度为 \(c\) 的区间和一些长度为 \(1\) 的区间。

这并不难,线段树暴力转移即可。

CODE

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10,INF=1e9;
inline int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w*=-1; ch=getchar(); }
	while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
	return s*w;
}
int n,c;
int sum[N];
int a[N],tr[4*N];
int dp[N];
inline void build(int k,int l,int r)
{
	if(l==r) { tr[k]=a[l]; return; }
	int mid=(l+r)>>1;
	build(k<<1,l,mid),build(k<<1|1,mid+1,r);
	tr[k]=min(tr[k<<1],tr[k<<1|1]);
}
inline int ask(int k,int l,int r,int x,int y)
{
	if(l>y||r<x) return INF;
	if(l>=x&&r<=y) return tr[k];
	int mid=(l+r)>>1;
	return min(ask(k<<1,l,mid,x,y),ask(k<<1|1,mid+1,r,x,y));  
}
signed main()
{
	n=read(),c=read();
	for(register int i=1;i<=n;i++) a[i]=read();
	for(register int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i];
	build(1,1,n);
	memset(dp,0x3f,sizeof(dp));
	dp[0]=0;
	for(register int i=1;i<=n;i++){
		dp[i]=min(dp[i],dp[i-1]+a[i]);
		if(i-c>=0) dp[i]=min(dp[i],dp[i-c]+sum[i]-sum[i-c]-ask(1,1,n,i-c+1,i));
	}
	printf("%lld\n",dp[n]);
	return 0;
}
posted @ 2021-11-02 21:11  ╰⋛⋋⊱๑落叶๑⊰⋌⋚╯  阅读(34)  评论(0编辑  收藏  举报