holiday题解
题目描述如下:
经过几个月辛勤的工作,FJ 决定让奶牛放假。假期可以在 1…N 天内任意选择一段(需要连
续),每一天都有一个享受指数 W。但是奶牛的要求非常苛刻,假期不能短于 P 天,否则奶
牛不能得到足够的休息;假期也不能超过 Q 天,否则奶牛会玩的腻烦。FJ 想知道奶牛们能
获得的最大享受指数。
Input(holiday.in)
第一行:N,P,Q.
第二行:N 个数字,中间用一个空格隔开。
Output(holiday.out)
一个整数,奶牛们能获得的最大享受指数。
Sample Input
5 2 4
-9 -4 -3 8 -6
Sample Output
5
Limitation
time:1s
memory:65536kb
50% 1≤N≤10000
100% 1≤N≤100000
1<=p<=q<=n
Hint
选择第 3-4 天,享受指数为-3+8=5。
这道题当时看的时候大概想了5分钟左右吧……结合最近学的RMQ和线段树。因为我们要求一段连续区间和的最大值,我们可以想到首先必然要枚举这个假期开始的日期,之后你的起始点确定,终点必然在[i+p-1,i+q-1]这段区间之内。这样的话其实范围就相对确定了,对于一段区间的和,我们可以先手初始化前缀和之后以相减的方法求出,而最关键的位置就在于如何确定终点?因为我们要求最大值,而前面一段区间的前缀和已经被确定,所以只要在[i+p-1,i+q-1]中找出前缀和最大值即可。而每段区间的最大值完全可以用线段树或者st表维护。这样就可以解决这个问题了。
#include<cstdio> #include<algorithm> #include<iostream> #include<cstring> #include<cmath> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar('\n') typedef long long ll; using namespace std; const int M = 200005; struct seg { int v; }t[M<<2]; ll n,p,q,a[M],maxn; bool flag; ll read() { ll ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } void build(ll p,ll l,ll r) { if(l == r) { t[p].v = a[l]; return; } ll mid = (l+r) >> 1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); t[p].v = max(t[p<<1].v,t[p<<1|1].v); } ll query(int p,int l,int r,int kl,int kr) { if(l == kl && r == kr) { return t[p].v; } ll mid = (l+r) >> 1; if(kr <= mid) return query(p<<1,l,mid,kl,kr); else if(kl > mid) return query(p<<1|1,mid+1,r,kl,kr); else return max(query(p<<1,l,mid,kl,mid),query(p<<1|1,mid+1,r,mid+1,kr)); } int main() { // freopen("holiday.in","r",stdin); // freopen("holiday.out","w",stdout); n = read(),p = read(),q = read(); rep(i,1,n) a[i] = read(),a[i] += a[i-1]; build(1,1,n); rep(i,1,n) { ll dl = i*1ll+p-1,dr = i*1ll+q-1; if(dl > n) break; if(dr > n) dr = n; ll k = query(1,1,n,dl,dr) - a[i-1]; if(!flag) maxn = k,flag = 1; else maxn = max(maxn,k); } printf("%lld\n",maxn); return 0; } /* 5 2 4 -9 -4 -3 8 -6 */
这段代码的时间复杂度是O(nlogn),有没有更好的做法?
有!因为我们在上面的做法中是维护最大值然后减前缀和,那么我们同样可以用前缀和去减前面的最小值!即f[i] = sum[i] - min(sum[1…i])。那么我们可以用单调队列来维护一个单调递增的序列之后每次用当前的值去减队首元素(最小值)即可。
这样的话由于每个点都进入队列一次,所以一共出队列必然<=n次,时间复杂度是O(n),最多不过O(2*n).
看一下代码
#include<cstdio> #include<cmath> #include<cstring> #include<iostream> #include<algorithm> #include<string> #include<map> #include<queue> #include<vector> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) typedef long long ll; using namespace std; const int M = 100005; ll a, n, p, Q, head = 1, tail, q[M], f[M], sum[M], ans = -0X3f3f3f3f; int main() { scanf("%lld %lld %lld", &n, &p, &Q); rep(i,1,n) scanf("%lld", &a),sum[i] = a + sum[i-1]; rep(i,p,n) { while(sum[q[tail]] >= sum[i-p] && head <= tail) tail--;//因为i-p处的元素必须入队,所以队中一切比sum[i-p]大的元素必须出队 q[++tail] = i - p;//把sum[i-p]压入队列 while(q[head] < i - Q) head++;//因为我们最多只能取到i-q这么长的区间,所以在这个区间之前的所有元素必须出列。 f[i] = sum[i] - sum[q[head]]; ans = max(ans, f[i]); } printf("%lld\n", ans); return 0; }