算法笔记-DP优化

前缀和优化

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 i1 的排列中插入 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,ji+1)jfi1,t 时间复杂度为 O ( n k 2 ) O(nk^2) O(nk2)

前缀和优化状态转移,每次 j j j 循环的时候,令变量 s u m sum sum 记录一下前缀和,判断 j − i + 1 j-i+1 ji+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 n1 个连接处。现在最多砍断 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=kj1fi1,t k k k 为满足 s u m j − s u m k ≤ x sum_j - sum_k \le x sumjsumkx 的最小的 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=0jfi,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=prei1,j1prei1,k1每次状态转移时需要先求出 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+1li

i i i 增大时,因为 l i + 1 ≥ l i l_{i+1} \ge l_i li+1li ,之前的不合法决策点现在还是不合法决策点,可以永远扔掉。

如果 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 apibpi),最多买入 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 个股票能够赚到的最多钱。

状态转移分为四种情况:

  1. 直接买
    f i , j = − a p i × j f_{i,j} = - ap_i \times j fi,j=api×j
  2. 不买也不卖
    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,fi1,j)
  3. 在之前基础上买
    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,fiw1,k(jk)×api)(jasik<j)
  4. 在之前基础上卖
    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,fiw1,k+(kj)×bpi)(j<kj+bsi)

因为情况3、4,时间复杂度为 O ( T ⋅ M a x p 2 ) O(T·Maxp^2) O(TMaxp2),因此需要优化。

观察到情况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+[djdi]}(jik) 时间复杂度为 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+(sisj1L)2}其中, s k = ∑ i = 1 k ( c i + 1 ) s_k = \sum\limits_{i=1}^k(c_i+1) sk=i=1k(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+(sisjL)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+si22si×L+(sj+L)22si×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}+si22si×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+(fisi2+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=fisi2+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=1m(vvk)2
v ‾ = 1 m ∑ i = 1 m v i \overline{v} = \frac{1}{m}\sum\limits_{i=1}\limits^mv_i v=m1i=1mvi 代入上式
得到: 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=1mvi)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=1mvi)2+m×k=1mvk2

第一项是定值,方差只跟第二项有关。
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,j1+(sumisumk)2}(0k<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;
	
}


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值