动态规划基础

等有空的时候再加讲解吧。如果仅仅是代码就能帮助到您那就太好了。

线性动态规划

最长(不)上升/下降子序列

这里就不介绍一般的方法了,一般的方法二重循环时间复杂度 O ( n 2 ) O(n^2) O(n2)确实是有点拉了。所以我们就介绍一种利用二分的方法,可以将时间复杂度降到 O ( n l o g n ) O(nlogn) O(nlogn)的方法。

就以最长上升子序列为例。比如我给出一个数列:

4   2   3   1   5 4\ 2\ 3\ 1\ 5 4 2 3 1 5

那么我们定义 d p [ i ] dp[i] dp[i]为:长度为 i i i的最长上升子序列的末尾元素的最小值。有点绕,我们直接来看是怎么实现的吧。

首先初始化 d p dp dp数组都为最大值,也就是:

i i i d p [ i ] dp[i] dp[i]
1 1 1 ∞ ∞
2 2 2 ∞ ∞
3 3 3 ∞ ∞
4 4 4 ∞ ∞
5 5 5 ∞ ∞
6 6 6 ∞ ∞

第一个元素是 4 4 4,那么就来找dp数组中第一个大于等于 4 4 4的数,然后把它替换为 4 4 4。这里很明显,第一个大于四的就是 d p [ 1 ] dp[1] dp[1],那么就把 d p [ 1 ] dp[1] dp[1]变为 4 4 4

i i i d p [ i ] dp[i] dp[i]
1 1 1 4 4 4
2 2 2 ∞ ∞
3 3 3 ∞ ∞
4 4 4 ∞ ∞
5 5 5 ∞ ∞
6 6 6 ∞ ∞

第二个元素是 2 2 2,第一个大于等于 2 2 2的是 d p [ 1 ] = 4 dp[1]=4 dp[1]=4,那么就再进行替换:

i i i d p [ i ] dp[i] dp[i]
1 1 1 2 2 2
2 2 2 ∞ ∞
3 3 3 ∞ ∞
4 4 4 ∞ ∞
5 5 5 ∞ ∞
6 6 6 ∞ ∞

第三个元素是 3 3 3,第一个大于等于 3 3 3的是 d p [ 2 ] = ∞ dp[2]=∞ dp[2]=,那么就再进行替换:

i i i d p [ i ] dp[i] dp[i]
1 1 1 2 2 2
2 2 2 3 3 3
3 3 3 ∞ ∞
4 4 4 ∞ ∞
5 5 5 ∞ ∞
6 6 6 ∞ ∞

第四个元素是 1 1 1,第一个大于等于 1 1 1的是 d p [ 1 ] = 2 dp[1]=2 dp[1]=2,那么就再进行替换:

i i i d p [ i ] dp[i] dp[i]
1 1 1 1 1 1
2 2 2 3 3 3
3 3 3 ∞ ∞
4 4 4 ∞ ∞
5 5 5 ∞ ∞
6 6 6 ∞ ∞

最后一个元素是 5 5 5,第一个大于等于 5 5 5的是 d p [ 3 ] = ∞ dp[3]=∞ dp[3]=,再进行替换:

i i i d p [ i ] dp[i] dp[i]
1 1 1 1 1 1
2 2 2 3 3 3
3 3 3 5 5 5
4 4 4 ∞ ∞
5 5 5 ∞ ∞
6 6 6 ∞ ∞

所有的元素都处理掉了,最后只要 f o r for for一遍看看直到几它都小于 ∞ ∞ ,很明显这里是 3 3 3。所以最长上升子序列长为 3 3 3

这里的代码实现使用了 S T L STL STL库里的 l o w e r _ b o u n d lower\_bound lower_bound函数。这个函数的意思就是:

int pos = (int)(lower_bound(dp + 1, dp + 1 + n, a[i]) - dp);

d p dp dp函数里面找到第一个大于等于 a [ i ] a[i] a[i]的值,并返回其地址。 u p p e r _ b o u n d upper\_bound upper_bound则是第一个大于的。

代码实现如下:

#include <iostream> 
#include <cstdio> 
#include <cstring> 
#include <algorithm> 
using namespace std;
int dp[100005];
int n, a[100005], ans;
bool cmp(int a, int b) { 
	return a > b;
}
int main() { 
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i]; 
	for(int i = 1; i <= n; i++) {
		int pos = (int)(upper_bound(dp + 1, dp + 1 + n, a[i], cmp) - dp);
		//这是最长不上升子序列,改为lower_bound就是最长下降子序列。
        dp[pos] = a[i];
    }
	for(int i = 0; i < 100005; i++) dp[i] = 2147483647; 
	for(int i = 1; i <= n; i++) {
		int pos = (int)(upper_bound(dp + 1, dp + 1 + n, a[i]) - dp);
		//这是最长不下降子序列,改为lower_bound就是最长上升子序列。
        dp[pos] = a[i];
    }
	for(int i = 1; i <= n; i++) { 
		if(dp[i] == 2147483647) break; 
		ans++;
	}
	cout << ans << endl; 
	return 0;
}

最长公共子序列

这个问题的状态转移方程真的超级超级好懂了。直接上代码吧。

#include <iostream> 
#include <cstdio> 
using namespace std;
int dp[1005][1005];
int n, m, a[1005], b[1005];
int max(int a, int b) { 
	if(a > b) return a;
	else return b; 
}
int main() {
	cin >> n >> m;
	for(int i = 1; i <= n; i++) cin >> a[i]; 
	for(int i = 1; i <= m; i++) cin >> b[i]; 
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= m; j++) {
			if(a[i] == b[j]) dp[i][j] = dp[i - 1][j - 1] + 1; 
			else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
		} 
	}
    cout << dp[n][m] << endl;
	return 0; 
}

但如果是 1 1 1~ n n n的某种排列的公共子序列,那可以让第二个序列对应第一个序列映射后求第二个序列的 L I S LIS LIS

L I S LIS LIS:最长上升子序列。

数学相关

划分数

划分数就是有 n n n个无区别的物品,将它们划分成不超过 m m m组, 求出划分方法模 M M M的余数。

#include <iostream> 
using namespace std;
int dp[1005][10005]; 
int n, m, M;
int main() {
	cin >> n >> m >> M;
	dp[0][0] = 1;
	for(int i = 1; i <= m; i++) {
		for(int j = 0; j <= n; j++) { //j的i划分 
			if(j - i >= 0) dp[i][j] = (dp[i - 1][j] + dp[i][j - i]) % M; 
			else dp[i][j] = dp[i - 1][j];
		} 
	}
    cout << dp[m][n] << endl;
}

多重集组合数

#include <iostream> 
using namespace std;
int dp[1005][10005]; 
int a[1005];
int n, m, M;
int main() {
	cin >> n >> m >> M;
	for(int i = 1; i <= n; i++) cin >> a[i]; 
	for(int i = 0; i <= n; i++) dp[i][0] = 1; 
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= m; j++) { 
			if(j - a[i] > 0)
                dp[i][j] = (dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1 - a[i]] + M) % M;
			else dp[i][j] = (dp[i][j - 1] + dp[i - 1][j]) % M; 
		}
	}
    cout << dp[m][n] << endl;
}

背包

01背包

Luogu P1048 [NOIP2005 普及组] 采药

01背包指的就是,我们有一个背包,然后有许多物品。背包是有一定的空间限制的,而每一个物品有相应的体积和价值。解决的问题就是在背包空间允许的范围内,背包所装的物品价值最大是多少。(好像做贼)

01背包的状态转移方程还是很简单的,无非就是选或者不选的问题。第 i i i个物品由第 i − 1 i-1 i1个物品转移而来,选或不选当然是要选价值最大的。

状态转移方程如下:

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − t [ i ] ] + w [ i ] ) dp[i][j] = max(dp[i-1][j],dp[i-1][j-t[i]]+w[i]) dp[i][j]=max(dp[i1][j],dp[i1][jt[i]]+w[i])

在这其中, t [ i ] t[i] t[i]表示这个物品所占的背包空间,在这个例题里就是采药所需要花的时间。而 w [ i ] w[i] w[i]表示的就是这个物品的价值,在这个题里就是药草的价值。 d p [ ] [ ] dp[][] dp[][]则表示这种方案的最大价值。

我觉得还是相当好理解的。 i i i表示第几个物品, j j j则表示还剩下的背包空间(在例题里就是还剩下多少时间)。 m a x max max函数里面有两项,第一项就是不选第 i i i个物品,所以背包空间不会发生变化,价值也不会变;第二项就是选择这个物品,把这个东西放进背包里,背包空间就会减小,而这种方案的价值就会增大。

下面的代码是已经压掉一维的代码。仔细想一下也可以知道,价值倒着来的话,实际上也就不需要第一维数组了,可以节省空间。

#include <iostream> 
#include <cstdio> 
using namespace std;
int T, M, w[1005], t[1005]; 
int dp[1005];
int max(int a, int b) { 
	if(a > b) return a;
	else return b; 
}
int main() {
	cin >> T >> M;
	for(int i = 1; i <= M; i++) cin >> t[i] >> w[i]; 
	for(int i = 1; i <= M; i++)
		for(int j = T; j >= t[i]; j--)
			dp[j] = max(dp[j], dp[j - t[i]] + w[i]);
    cout << dp[T] << endl;
	return 0; 
}

完全背包

Luogu P1616 疯狂的采药

每个东西不一定只能拿一个了!

如果不压掉维度的话,状态转移方程就是:

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − k ∗ t [ i ] ] + k ∗ w [ i ] ) dp[i][j]=max(dp[i-1][j-k*t[i]]+k*w[i]) dp[i][j]=max(dp[i1][jkt[i]]+kw[i])

k = 0 , 1 , 2 , … k=0,1,2,… k=0,1,2,

所以不同点就在于要枚举 k k k值。

压掉一维的写法上与01背包区别就只是循环顺序的不同。

#include <iostream> 
#include <cstdio> 
using namespace std;
int T, M, w[1005], t[1005]; 
int dp[1005];
int max(int a, int b) { 
	if(a > b) return a;
	else return b; 
}
int main() {
	cin >> T >> M;
	for(int i = 1; i <= M; i++) cin >> t[i] >> w[i]; 
	for(int i = 1; i <= M; i++)
		for(int j = t[i]; j <= T; j++)
			dp[j] = max(dp[j], dp[j - t[i]] + w[i]);
    cout << dp[T] << endl;
	return 0; 
}

区间DP

一般是三重循环,第一个是枚举长度,第二个枚举起点,第三个枚举切割点

也就是:

d p [ j ] [ j + i − 1 ] = m a x ( d p [ j ] [ k ] + d p [ k + 1 ] [ j + i − 1 ] + w [ i ] [ j + i − 1 ] , d p [ j ] [ j + i − 1 ] ) dp[j][j + i - 1] = max(dp[j][k] + dp[k + 1][j + i - 1] + w[i][j + i - 1], dp[j][j + i - 1]) dp[j][j+i1]=max(dp[j][k]+dp[k+1][j+i1]+w[i][j+i1],dp[j][j+i1])

CF607B Zuma

#include <iostream>
#include <cstring>
using namespace std;

int dp[505][505];
int n, c[505];
int min(int a, int b) {
    if(a < b) return a;
    else return b;
}

int main() {
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> c[i];
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            dp[i][j] = 2147483647;
    for(int i = 1; i <= n; i++) dp[i][i] = 1;
    for(int i = 1; i < n; i++) {
        if(c[i] == c[i + 1]) dp[i][i + 1] = 1;
        else dp[i][i + 1] = 2;
    }
    for(int l = 3; l <= n; l++) {
        for(int i = 1; i + l - 1 <= n; i++) {
            int j = i + l - 1;
            if(c[i] == c[j]) dp[i][j] = dp[i + 1][j - 1];
            for(int k = i; k < j; k++)
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
        }
    }
    cout << dp[1][n] << endl;
    return 0;
}

状压DP

Luogu P1433 吃奶酪

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;

double xx[20], yy[20], ans;
double a[20][20];
double dp[20][34000];

double min(double a, double b) {
    if(a < b) return a;
    else return b;
}

int n;

double distan(int x, int y) {
    return sqrt((xx[x] - xx[y]) * (xx[x] - xx[y]) + (yy[x] - yy[y]) * (yy[x] - yy[y]));
}

int main() {
    memset(dp, 127, sizeof(dp));
    ans = dp[0][0];
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> xx[i] >> yy[i];
    for(int i = 0; i <= n; i++)
        for(int j = i + 1; j <= n; j++) {
            a[i][j] = distan(i, j);
            a[j][i] = a[i][j];
        }
    for(int i = 1; i <= n; i++) dp[i][1 << (i - 1)] = a[0][i];
    for(int s = 1; s < (1 << n); s++) {
        for(int i = 1; i <= n; i++) {
            if(!(s & (1 << (i - 1)))) continue;
            for(int j = 1; j <= n; j++) {
                if(!(s & (1 << (j - 1)))) continue;
                if(i == j) continue;
                dp[i][s] = min(dp[i][s], dp[j][s - (1 << (i - 1))] + a[i][j]);
            }
        }
    }
    for(int i = 1; i <= n; i++)
        ans = min(ans, dp[i][(1 << n) - 1]);
    printf("%.2lf\n", ans);
    return 0;
}

树形DP

Luogu P1352 没有上司的舞会

#include <iostream>
using namespace std;

int n, root;
int r[6001], dp[6001][2], son[5001][5001], cnt[6001], tmpa, tmpb;
bool fla[6001];

int max(int a, int v) {
    if(a > v) return a;
    else return v;
}

void f(int x) {
    dp[x][0] = 0;
    dp[x][1] = r[x];
    for(int i = 1; i <= cnt[x]; i++) {
        f(son[x][i]);
        dp[x][0] += max(dp[son[x][i]][0], dp[son[x][i]][1]);
        dp[x][1] += dp[son[x][i]][0];
    }
}

int main() {
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> r[i];
    for(int i = 1; i <= n - 1; i++) {
        cin >> tmpa >> tmpb;
        son[tmpb][++cnt[tmpb]] = tmpa;
        fla[tmpa] = true;
    }
    for(int i = 1; i <= n; i++) {
        if(!fla[i]) {
            root = i;
            break;
        }
    }
    f(root);
    cout << max(dp[root][0], dp[root][1]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值