前缀和优化
P2513 [HAOI2009]逆序对数列
题意:
询问逆序对数为 k k k 的 n n n 的排列的个数 k k k k
解析:
令 f i , j f_{i, j} fi,j 为 i i i 的排列中逆序对为 j j j 的个数
在 i − 1 i-1 i−1 的排列中插入 i i i ,状态转移为 f i , j = ∑ t = m a x ( 0 , j − i + 1 ) j f i − 1 , t f_{i,j} = \sum\limits_{t = max(0,j-i+1)}\limits^j f_{i-1,t} fi,j=t=max(0,j−i+1)∑jfi−1,t 时间复杂度为 O ( n k 2 ) O(nk^2) O(nk2)
前缀和优化状态转移,每次 j j j 循环的时候,令变量 s u m sum sum 记录一下前缀和,判断 j − i + 1 j-i+1 j−i+1 时候大于零,大于零时减去不需要求和的项,然后令 f i , j = s u m f_{i,j} = sum fi,j=sum
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e3+10;
const int mod = 1e4;
int f[maxn][maxn];
int n, k;
int main(){
cin >> n >> k;
f[1][0] = 1;
for(int i = 2; i <= n; i++){
int sum = 0;
for(int j = 0; j <= k; j++){
if(j - i + 1 > 0)
sum -= f[i-1][j-i];
sum = ((sum%mod)+mod)%mod;
sum += f[i-1][j];
sum %= mod;
f[i][j] = sum;
}
}
cout << f[n][k] << endl;
return 0;
}
P2511 [HAOI2008]木棍分割
题意:
n n n 根木棍连在一起,有 n − 1 n-1 n−1 个连接处。现在最多砍断 m m m 个连接处,使长度最大的一段木根长度最小,并求出方案数
解析:
使长度最大的一段木根最小,可以二分求出这个长度 x x x
令 f i , j f_{i,j} fi,j 表示前 j j j 个木根分成 i i i 组的方案数,则 f i , j = ∑ t = k j − 1 f i − 1 , t f_{i,j} = \sum\limits_{t=k}\limits^{j-1} f_{i-1,t} fi,j=t=k∑j−1fi−1,t k k k 为满足 s u m j − s u m k ≤ x sum_j - sum_k \le x sumj−sumk≤x 的最小的 k k k,时间复杂度为 O ( n 3 ) O(n^3) O(n3)
令 p r e i , j = ∑ t = 0 j f i , t pre_{i,j} = \sum\limits_{t=0}\limits^j f_{i,t} prei,j=t=0∑jfi,t,即 p r e pre pre 为 f f f 的前缀和,则 f i , j = p r e i − 1 , j − 1 − p r e i − 1 , k − 1 f_{i,j} = pre_{i-1,j-1}-pre_{i-1,k-1} fi,j=prei−1,j−1−prei−1,k−1每次状态转移时需要先求出 k k k,时间复杂度并没有优化。
注意到相同的 j j j 对应同样的 k k k ,可以预处理出每个 j j j 对应的 k k k 。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5+10;
const int mod = 10007;
int n, m, ans;
int maxx;
int a[maxn], sum[maxn], lef[maxn];
int dp[maxn], pre[maxn];
int check(int x){
int cnt = 0, len = 0;
for(int i = 1; i <= n; i++){
if(len + a[i] > x){
cnt++;
len = a[i];
}
else
len += a[i];
}
return cnt <= m;
}
int DP(int x){
int k = 0, res = 0;
for(int i = 1; i <= n; i++){
for(; k < i; k++)
if(sum[i] - sum[k] <= x){
lef[i] = k;
break;
}
}
for(int i = 1; i <= n; i++){
if(sum[i] <= x)
dp[i] = 1;
pre[i] = (pre[i-1] + dp[i]) % mod;
}
for(int i = 2; i <= m+1; i++){
for(int j = 1; j <= n; j++){
dp[j] = pre[j-1];
if(lef[j]-1 >= 0)
dp[j] = dp[j] - pre[lef[j]-1];
dp[j] = ((dp[j] % mod) + mod) % mod;
}
res = (res + dp[n]) % mod;
for(int j = 1; j <= n; j++)
pre[j] = (pre[j-1] + dp[j]) % mod;
}
res += (sum[n] <= x);
res %= mod;
return res;
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> a[i];
maxx = max(maxx, a[i]);
sum[i] = sum[i-1] + a[i];
}
int l = maxx, r = sum[n];
while(l <= r){
int mid = (l+r) >> 1;
if(check(mid)){
ans = mid;
r = mid-1;
}
else
l = mid+1;
}
cout << ans << " " << DP(ans) << endl;
return 0;
}
单调队列优化
状态转移方程形如 f i = m i n / m a x t = l i i { g i } + w i f_i = min/max_{t = l_i}^{i}\{g_i\} + w_i fi=min/maxt=lii{gi}+wi 且 l i + 1 ≥ l i l_{i+1} \ge l_i li+1≥li
当 i i i 增大时,因为 l i + 1 ≥ l i l_{i+1} \ge l_i li+1≥li ,之前的不合法决策点现在还是不合法决策点,可以永远扔掉。
如果 j < j ′ j < j' j<j′ 且 d p j < d p j ′ dp_j < dp_{j'} dpj<dpj′ ,则 j j j 也可以扔掉。因为 j j j 一定会先成为不合法决策点,且 j ′ j' j′ 更优,所以 j j j 一定不会是最优决策点。
P2569 [SCOI2010]股票交易
题意:
股票第 i i i 天买入价为 a p i ap_i api ,卖出价为 b p i bp_i bpi ( a p i ≥ b p i ap_i \ge bp_i api≥bpi),最多买入 a s i as_i asi ,最多卖出 b s i bs_i bsi。两次交易至少间隔 w w w 天。
再第一天之前,钱的数目无限,在 T T T 天后,赚最多的钱。
分析:
令 f i , j f_{i,j} fi,j 表示在第 i i i 天拥有 j j j 个股票能够赚到的最多钱。
状态转移分为四种情况:
- 直接买
f i , j = − a p i × j f_{i,j} = - ap_i \times j fi,j=−api×j - 不买也不卖
f i , j = m a x ( f i , j , f i − 1 , j ) f_{i,j} = max(f_{i,j}, f_{i-1,j}) fi,j=max(fi,j,fi−1,j) - 在之前基础上买
f i , j = max k ( f i , j , f i − w − 1 , k − ( j − k ) × a p i ) ( j − a s i ≤ k < j ) f_{i,j} = \max\limits_k(f_{i,j}, f_{i-w-1,k} - (j-k) \times ap_i)\quad(j-as_i \le k < j) fi,j=kmax(fi,j,fi−w−1,k−(j−k)×api)(j−asi≤k<j) - 在之前基础上卖
f i , j = max k ( f i , j , f i − w − 1 , k + ( k − j ) × b p i ) ( j < k ≤ j + b s i ) f_{i,j} = \max\limits_k(f_{i,j}, f_{i-w-1,k} + (k-j) \times bp_i)\quad(j < k \le j+bs_i) fi,j=kmax(fi,j,fi−w−1,k+(k−j)×bpi)(j<k≤j+bsi)
因为情况3、4,时间复杂度为 O ( T ⋅ M a x p 2 ) O(T·Maxp^2) O(T⋅Maxp2),因此需要优化。
观察到情况3、4的状态转移方程可以用单调队列优化,维护一个价值递减的队列。
注意情况3应该顺序,情况4应该逆序。(因为单调队列:之前的不合法决策点之后一定是不合法决策点)
代码:
#include<iostream>
#include<cstring>
using namespace std;
typedef long long ll;
const int maxn = 2e3+10;
const int INF = 0x3f3f3f3f;
int f[maxn][maxn];
int T, w, maxp;
int ap[maxn], bp[maxn], as[maxn], bs[maxn], q[maxn];
int main(){
cin >> T >> maxp >> w;
memset(f, -INF, sizeof(f));
for(int i = 1; i <= T; i++)
cin >> ap[i] >> bp[i] >> as[i] >> bs[i];
for(int i = 1; i <= T; i++){
for(int j = 0; j <= as[i]; j++)
f[i][j] = -1*j*ap[i];
for(int j = 0; j <= maxp; j++)
f[i][j] = max(f[i][j], f[i-1][j]);
if(i-w-1 < 0)
continue;
int hh = 1, tt = 0;
for(int j = 0; j <= maxp; j++){
while(hh<=tt&&q[hh]<j-as[i])
hh++;
int k = q[hh];
if(hh<=tt)
f[i][j] = max(f[i][j], f[i-w-1][k]+k*ap[i]-j*ap[i]);
while(hh<=tt&&f[i-w-1][q[tt]]+q[tt]*ap[i]<=f[i-w-1][j]+j*ap[i])
tt--;
q[++tt] = j;
}
hh = 1, tt = 0;
for(int j = maxp; j >= 0; j--){
while(hh<=tt&&q[hh]>j+bs[i])
hh++;
int k = q[hh];
if(hh<=tt)
f[i][j] = max(f[i][j], f[i-w-1][k]+bp[i]*(k-j));
while(hh<=tt&&f[i-w-1][j]+bp[i]*j>=f[i-w-1][q[tt]]+bp[i]*q[tt])
tt--;
q[++tt] = j;
}
}
int ans = -1;
for(int i = 0; i <= maxp; i++)
ans = max(ans, f[T][i]);
printf("%d\n", ans);
return 0;
}
P3572 [POI2014]PTA-Little Bird
题意:
给定 n n n 棵树的高度 d i d_i di ,当飞到大于等于当前树的高度时劳累值加1。 q q q 次询问,每次询问给定 k k k ,每次最多往后飞 k k k 棵树,求最小劳累值
解析:
对于每次询问,令 f i f_{i} fi 为飞到第 i i i 棵树的最小劳累值。则状态转移方程为 f i = min j { f j + [ d j ≤ d i ] } ( j ≥ i − k ) f_i = \min\limits_j\{ f_j + [d_j \le d_i] \} \quad(j \ge i-k) fi=jmin{fj+[dj≤di]}(j≥i−k) 时间复杂度为 O ( q n 2 ) O(qn^2) O(qn2) 。但状态转移可以单调队列优化,最后时间复杂度为 O ( q n ) O(qn) O(qn) 。
注意的是:
- 在单调队列中, f f f 是单调不下降
- 相同的 f f f , d d d 大的更靠近队首
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e6+10;
int d[maxn], f[maxn];
int q[maxn], hh, tt;
int n, T;
void solve(int k){
hh = 1, tt = 0;
q[++tt] = 1;
for(int i = 2; i <= n; i++){
while(hh <= tt && i - q[hh] > k)
hh++;
f[i] = f[q[hh]] + (d[q[hh]] <= d[i]);
while(hh <= tt && (f[q[tt]] > f[i] || (f[q[tt]] == f[i] && d[q[tt]] <= d[i])))
tt--;
q[++tt] = i;
}
//cout << "ans = ";
cout << f[n] << endl;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++)
cin >> d[i];
cin >> T;
while(T--){
int k;
cin >> k;
solve(k);
}
}
斜率优化
对于这样的 d p dp dp 方程: d p i = m i n ( a i × b j + c j + d i ) dp_i = min(a_i\times b_j + c_j + d_i) dpi=min(ai×bj+cj+di) ,其中 b b b 严格单调递增,可以考虑使用斜率优化
以 P3195 [HNOI2008]玩具装箱 为例:
令 f i f_i fi 为前 i i i 个玩具装好的最小花费,状态转移方程为: f i = min j { f j + ( s i − s j − 1 − L ) 2 } f_i = \min\limits_j\{f_j + (s_i-s_j-1-L)^2\} fi=jmin{fj+(si−sj−1−L)2}其中, s k = ∑ i = 1 k ( c i + 1 ) s_k = \sum\limits_{i=1}^k(c_i+1) sk=i=1∑k(ci+1)
将 L L L 提前+1,且将 m i n min min 去掉,则 f i = f j + ( s i − s j − L ) 2 f_i = f_j+(s_i-s_j-L)^2 fi=fj+(si−sj−L)2
化简整理得: f i = f j + s i 2 − 2 s i × L + ( s j + L ) 2 − 2 s i × s j f_i = f_j+s_i^2-2s_i\times L + (s_j+L)^2 - 2s_i\times s_j fi=fj+si2−2si×L+(sj+L)2−2si×sj
项可以分为三类,只与 i i i 有关,只与 j j j 有关,与 i , j i,j i,j 都有关。将同类的项放在一起: f i = − 2 s i × s j + { f j + ( s j + L ) 2 } + s i 2 − 2 s i × L f_i = - 2s_i\times s_j+\{f_j+ (s_j+L)^2\}+s_i^2-2s_i\times L fi=−2si×sj+{fj+(sj+L)2}+si2−2si×L
进行移项,形如
y
=
k
x
+
b
y = kx+b
y=kx+b 的形式
将
f
u
n
c
(
i
)
×
f
u
n
c
(
j
)
func(i)\times func(j)
func(i)×func(j) 看成
k
×
x
k \times x
k×x ,
f
i
f_i
fi 的项出现在
b
b
b 中,
f
u
n
c
(
j
)
func(j)
func(j) 的项在
y
y
y 中。如果
x
x
x 的表达式单调递减,等式两边同乘-1,变为单调递增。
状态转移方程可以变形为: f j + ( s j + L ) 2 = 2 s i × s j + ( f i − s i 2 + 2 s i × L ) f_j+ (s_j+L)^2 = 2s_i\times s_j + (f_i-s_i^2+2s_i\times L) fj+(sj+L)2=2si×sj+(fi−si2+2si×L)则 y = f j + ( s j + L ) 2 y = f_j+ (s_j+L)^2 y=fj+(sj+L)2 , k = 2 s i k = 2s_i k=2si , x = s j x = s_j x=sj , b = f i − s i 2 + 2 s i × L b =f_i-s_i^2+2s_i\times L b=fi−si2+2si×L
根据线性规划,最优决策点位于下凸包的点集上。
可以用单调队列维护凸包:
- 在凸包上找到最优决策点 j j j
- 使用 j j j 更新 i i i
- 将 i i i 放入图形,并更新凸包
本题中,转移方程具有决策单调性,可以采用四边形不等式证明,此处暂且不证明。
因为具有决策单调性,可以单调队列维护。在选择最优决策点时,当队首第一条线段斜率小于等于 k i k_i ki 时,队首出队,此时队首为最优决策点。
P3195 [HNOI2008]玩具装箱
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 5e4+10;
ll f[maxn], s[maxn], q[maxn], ans;
ll n, L;
ll x(ll a){ return s[a];}
ll y(ll a){ return f[a]+(s[a]+L)*(s[a]+L);}
ll k(ll a){ return 2*s[a];}
long double slope(int i, int j){
return (long double)(y(j)-y(i))/(x(j)-x(i));
}
int main(){
cin >> n >> L;
L++;
for(int i = 1; i <= n; i++)
cin >> s[i];
for(int i = 1; i <= n; i++)
s[i] += s[i-1]+1;
int h = 1, t = 0; q[++t] = 0;
for(int i = 1; i <= n; i++){
while(h < t && slope(q[h], q[h+1]) <= k(i))
h++;
f[i] = f[q[h]] + (s[i]-s[q[h]]-L)*(s[i]-s[q[h]]-L);
while(h < t && slope(q[t-1], q[t]) >= slope(q[t], i))
t--;
q[++t] = i;
}
cout << f[n] << endl;
return 0;
}
P2900 [USACO08MAR]Land Acquisition G
题意:
n n n 块土地,单独买的价格为土地面积;并购一组土地的价格为最大的长与最大的宽之积。询问最小费用。
解析:
首先可以考虑到,对于一块土地 i i i,如果存在土地 j j j 的长、宽均大于土地 i i i 的,则土地 i i i 对答案没有任何贡献。
因此将所有土地按长度递减排序,长度相同的按宽度递减排序。然后遍历土地,将宽度小于等于当前土地的土地删除。留下的土地长度递减,宽度递增。
在最优决策下,一组的土地一定是连续的。假设不连续,则间断的那一部分土地长度小于开头,宽度小于结尾,一起买相当于可以白嫖间断的那一部分。
令
f
i
f_i
fi 为前
i
i
i 个土地最小费用,则状态转移方程
f
i
=
m
i
n
{
f
j
+
w
j
+
1
×
l
i
}
f_i = min\{f_j + w_{j+1} \times l_i\}
fi=min{fj+wj+1×li} 显然是
O
(
n
2
)
O(n^2)
O(n2) 的时间复杂度。因为有
w
j
+
1
×
l
i
w_{j+1} \times l_i
wj+1×li 项,可以考虑斜率优化。
f
j
=
l
i
×
(
−
w
j
+
1
)
+
f
i
f_j = l_i \times (-w_{j+1}) +f_i
fj=li×(−wj+1)+fi排序之后,
w
w
w 是递减的,
l
l
l 是递增的,则
−
w
-w
−w 是递增的。
斜率 k k k 和 x x x 均递增,可以用单调队列来维护 。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5+10;
#define int ll
const int INF = 0x3f3f3f3f;
struct Land{
int x, y;
}land[maxn];
int n;
int stk[maxn], stop;
int q[maxn], h = 1, t;
vector<int> s;
int f[maxn];
bool cmp(Land a, Land b){
if(a.x == b.x)
return a.y > b.y;
else
return a.x > b.x;
}
long double slope(int i, int j){
return (long double)(f[j]-f[i])/(land[i+1].x-land[j+1].x);
}
signed main(){
cin >> n;
for(int i = 1; i <= n; i++)
cin >> land[i].x >> land[i].y;
sort(land+1, land+n+1, cmp);
int tot = 0;
for(int i = 1; i <= n; i++){
if(land[i].y > land[tot].y)
land[++tot] = land[i];
}
n = tot;
q[++t] = 0;
for(int i = 1; i <= n; i++){
while(h < t && slope(q[h], q[h+1]) <= land[i].y)
h++;
f[i] = f[q[h]] + land[q[h]+1].x * land[i].y;
while(h < t && slope(q[t-1], q[t]) >= slope(q[t], i))
t--;
q[++t] = i;
}
cout << f[n] << endl;
return 0;
}
P4072 [SDOI2016]征途
题意:
n n n 段路, m m m 天走完,使每一天走的路长度的方差最小。设方差为 v v v ,输出 v × m 2 v\times m^2 v×m2
解析:
首先,
s
2
=
1
m
∑
k
=
1
m
(
v
‾
−
v
k
)
2
s^2 = \frac{1}{m} \sum\limits_ {k=1}\limits^m(\overline{v}-v_k)^2
s2=m1k=1∑m(v−vk)2
将
v
‾
=
1
m
∑
i
=
1
m
v
i
\overline{v} = \frac{1}{m}\sum\limits_{i=1}\limits^mv_i
v=m1i=1∑mvi 代入上式
得到:
s
2
=
1
m
{
−
1
m
(
∑
i
=
1
m
v
i
)
2
+
(
v
1
2
+
v
2
2
+
.
.
.
+
v
m
2
)
}
s^2 = \frac{1}{m}\{-\frac{1}{m}(\sum\limits_{i=1}\limits^mv_i)^2 +(v_1^2+v_2^2+...+v_m^2)\}
s2=m1{−m1(i=1∑mvi)2+(v12+v22+...+vm2)}
最后,
s
2
×
m
2
=
−
(
∑
i
=
1
m
v
i
)
2
+
m
×
∑
k
=
1
m
v
k
2
s^2 \times m^2 = -(\sum\limits_{i=1}\limits^mv_i)^2 + m\times\sum_{k=1}\limits^mv_k^2
s2×m2=−(i=1∑mvi)2+m×k=1∑mvk2
第一项是定值,方差只跟第二项有关。
令
f
i
,
j
f_{i,j}
fi,j 为前
i
i
i 段路分厂
j
j
j 天走的最小平方和
则 f i , j = min k { f k , j − 1 + ( s u m i − s u m k ) 2 } ( 0 ≤ k < i ) f_{i,j} = \min\limits_k\{ f_{k,j-1} + (sum_i-sum_k)^2\}\quad(0\le k < i) fi,j=kmin{fk,j−1+(sumi−sumk)2}(0≤k<i)因为有 s u m i × s u m k sum_i \times sum_k sumi×sumk 项,可以考虑用斜率优化。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 3e3+10;
int n, m;
int sum[maxn];
int f[maxn][2];
int q[maxn], hh, tt;
long double slope(int i, int j){
return (long double)(f[j][0]-f[i][0]+sum[j]*sum[j]-sum[i]*sum[i])/(sum[j]-sum[i]);
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
int x; cin >> x;
sum[i] = sum[i-1] + x;
f[i][0] = sum[i]*sum[i];
}
for(int i = 2; i <= m; i++){
hh = 1, tt = 0;
q[++tt] = i-1;
for(int j = i; j <= n; j++){
while(hh < tt && slope(q[hh], q[hh+1]) < 2*sum[j])
hh++;
f[j][1] = f[q[hh]][0] + (sum[j] - sum[q[hh]]) * (sum[j] - sum[q[hh]]);
while(hh < tt && slope(q[tt-1], q[tt]) > slope(q[tt], j))
tt--;
q[++tt] = j;
}
for(int j = 1; j <= n; j++)
f[j][0] = f[j][1];
}
cout << -sum[n]*sum[n] + m*f[n][1] << endl;
return 0;
}