2020安徽省大学生程序设计大赛题解——C 前后相加
C 前后相加
给定一个序列,长度为N,开始时,上面所有元素为0。你可以对序列作如下两种操作:
1.指定一个整数k (1<=k<=N)和一个非降序列c1, c2,c3,…, ck,(ci非负,1<=i<=k),对序列x的前k个数,令 xi=ci+xi。
2.指定一个整数k (1<=k<=N)和一个非升序列c1, c2,c3,…, ck,(ci非负,1<=i<=k),对序列x的后k 个数,令 x[N-k+i]=ci+x[N-k+i]。
你的目标是将序列x构造为与序列A相等的序列,即 xi=Ai(1<=i<=n),输出最少需要多少此操作,以达成目标。
题目
标签
动态规划,二分法
分析
显然我们可以让前缀加非 0 0 0的位置不相交,后缀加非 0 0 0的位置不相交,以获得最优的解决策略。
基于这样的叠加,本问题等价于把原序列拆成两个序列 x i {x_i} xi, y i {y_i} yi,满足:
- x 0 = y 0 = x n + 1 = y n + 1 = 0 x0=y0=xn+1=yn+1=0 x0=y0=xn+1=yn+1=0
- x i ⩾ 0 , y i ⩾ 0 , x i + y i = a i xi⩾0,yi⩾0,xi+yi=ai xi⩾0,yi⩾0,xi+yi=ai
满足上述要求的序列有多个,我们想要的是 ∑ [ x i > x i + 1 ] + [ y i < y i + 1 ] ∑[xi>xi+1]+[yi<yi+1] ∑[xi>xi+1]+[yi<yi+1]最小化的情况对应的叠加序列个数。
我们设 f i , j f_{i,j} fi,j表示考虑到序列的第 i i i位, x i x_i xi的值为 j j j的最小代价。那么我们不难发现两个性质:
- f i , j f_{i,j} fi,j的极差小于等于 2 2 2
- f i , j f_{i,j} fi,j单调不增
基于这两个性质,我们可以建立该问题动态规划的状态转移,然后记录一下分界点,转移二分一下就行了。
参考答案(C++)
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
#define calc(xls, fold) ((fold) + (xls > x) + (a - xls < na - x))
typedef class dp_t {
public:
int v0, p1, p2, a;
dp_t() {}
dp_t(int v0, int p1, int p2, int a) : v0(v0), p1(p1), p2(p2), a(a) {}
int eval(int x, int na) {
int rt = calc(0, v0);
if (p1 <= a)
rt = min(rt, calc(p1, v0 - 1));
if (p2 <= a)
rt = min(rt, calc(p2, v0 - 2));
return rt;
}
dp_t trans(int na) {
dp_t rt(0, 0, 0, na);
rt.v0 = eval(0, na);
int l = 0, r = na, mid;
while (l <= r) {
mid = (l + r) >> 1;
if (eval(mid, na) == rt.v0) {
l = mid + 1;
}
else {
r = mid - 1;
}
}
rt.p1 = l;
r = na;
while (l <= r) {
mid = (l + r) >> 1;
if (eval(mid, na) == rt.v0 - 1) {
l = mid + 1;
}
else {
r = mid - 1;
}
}
rt.p2 = l;
return rt;
}
} dp_t;
int n;
int a[N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> *(a + i);
}
dp_t f(0, 1, 1, 0);
for (int i = 1; i <= n; i++)
f = f.trans(a[i]);
f = f.trans(0);
cout << f.v0;
return 0;
}