做F2之前我们先想想F1 怎么做 做了这两题可以在想题的时候更好地梳理自己的思路 以得到优化算法的方法
因为F1数据小 可以n^3暴力 dp[i][j] 表示我们选到了第i个物品 并且选了第i个以后我们有j个物品 以下是代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e3+100;
typedef long long ll;
ll dp[N][N],a[N];
int main(){
int n,k,x;
scanf("%d%d%d",&n,&k,&x);
for(int i = 1; i <= n; i++) scanf("%lld",&a[i]);
if((n/k)>x){
puts("-1");
return 0;
}
int lim = 1;
for(int i = 1; i <= n; i++){
for(int j = lim; j <= min(i,x); j++){
for(int g = max(i-k,0); g < i; g++)//可优化部分
dp[i][j]=max(dp[i][j],dp[g][j-1]+a[i]);
}
if(i%k==0)lim++;
}
ll ans = 0;
for(int i = n-k+1; i <= n; i++) ans=max(ans,dp[i][x]);
printf("%lld\n",ans);
return 0;
}
观察代码:前两层循环是必不可少的 关键在于优化第三层循环 是不是感觉走了很多 没用的 当我遍历到j的时候 往前找k个寻求最大的dp[g][j-1] 来转移 那我们其实只要最大的那个 其他的都没用必要 遍历 那如何优化呢 显然要利用这个单调性
一个好的方法是观察 dp表的转移 下面我们通过输出 上面代码的 dp[i][j] 来观察
for(int i = 1; i <= n; i++){
for(int j = 1; j <= x; j++){
if(j==x) printf("%lld\n",dp[i][j]);
else printf("%lld ",dp[i][j]);
}
}
以第一个样例为例子:
有图中的转移关系我们可以看到 dp[4][3]由dp[2][2]和dp[3][2]转移来 但是 明显dp[3][2]大于dp[2][2] 所以dp[2][2]始终不会作为更新的跳板 (对于dp[3][2]及其后面的所有转移来说) 于是我们可以维护一个单调递减的队列 队头是最大值 如果队头到当前更新的dp[i][j]的距离大于 k 说明它已经不合适了 队头出队 然后维护单调性就行 这样n^3的复杂度就下降到了n^2
#include<bits/stdc++.h>
using namespace std;
const int N = 5e3+100;
typedef long long ll;
//http://codeforces.com/problemset/problem/1077/F2
ll dp[N][N],a[N];
ll q[N];
int main(){
int n,k,x;
scanf("%d%d%d",&n,&k,&x);
for(int i = 1; i <= n; i++) scanf("%lld",&a[i]);
if((n/k)>x){
puts("-1");
return 0;
}
memset(dp,-0x3f,sizeof(dp));
dp[0][0]=0;
for(int i = 1; i <= x; i++){
int l=1,r=0;
q[++r]=0;
for(int j = 1; j <= n; j++){//优化后变成了n^2 在递推时画出dp表
//抓住递推的单调性可以极大的优化时间复杂度
while(l<=r&&q[l]+k<j) l++;
dp[i][j]=dp[i-1][q[l]]+a[j];
while(l<=r&&dp[i-1][q[r]]<=dp[i-1][j]) r--;
q[++r]=j;
}
}
ll ans = 0;
for(int i = n-k+1; i <= n; i++) ans=max(ans,dp[x][i]);
printf("%lld\n",ans);
return 0;
}