dp笔记

dp笔记

1.线性dp

1.LIS

时间复杂度为 \(O(n\log n)\)

点击查看代码
#include <bits/stdc++.h>
#define N 10010
using namespace std;

int n, k, ans, a[N], g[N];

inline int read(){
   int s = 0, w = 1;
   char ch = getchar();
   for (; ch < '0' || ch > '9'; w *= ch == '-' ? -1 : 1, ch = getchar());
   for (; ch >= '0' && ch <= '9'; s = s * 10 + ch - '0', ch = getchar());
   return s * w;
}

int main(){
   n = read();
   for (int i = 1; i <= n; ++i) a[i] = read();
   k = 1, g[k] = a[1];//k代表LIS的长度,g数组记录每个长度下的最小数 
   for (int i = 2; i <= n; ++i){
   	int l = 1, r = k + 1;
   	if (a[i] > g[k]){
   		g[++k] = a[i];//进行记录 
   		continue;
   	}
   	while (l <= r){//二分求解降低时间复杂度 
   		int mid = (r + l) >> 1;
   		if (g[mid] < a[i]) l = mid + 1;
   		else r = mid - 1;
   	}
   	g[l] = a[i];//进行记录 
   }
   printf("%d\n", k);
   return 0;
}

2.LCIS

时间复杂度为 \(O(n^2)\)


3.LIS的数量

题意:

分析:

code:


2.区间dp

1.关路灯

题意:

一条马路上有 \(n\)盏路灯, 第 \(i\)盏路灯在位置 \(a_i\), 每秒钟会消耗 \(w_i\)的能源. 现在你在位置 \(m\)处, 每秒钟可以移动一个单位距离. 当你走到路灯所在的位置后你可以把路灯关掉, 现在你需要把所有的路灯都关掉, 此时消耗的最少能源是多少?

分析:

设关掉 \(l\)\(r\)的路灯后消耗的能源为 \(f_{i,j,0/1}\)(其中 \(0\)表示关完灯后在灯的左端,\(1\)表示关完灯后在灯的右端)。再维护前缀和数组 \(sum\),记录消耗的能源。由此,可以列出转移方程:

\(f_{i,j,0}=\begin{cases}\min(f_{i+1,j,0}+(a_{i+1}-a_i)*(sum_i+sum_n-sum_j)\\f_{i+1,j,1}+(a_j-a_i)*(sum_i+sum_n-sum_j))\end{cases}\\f_{i,j,1}=\begin{cases}\min(f_{i,j-1,1}+(a_j-a_{j-1})*(sum_{i-1}+sum_n-sum_{j-1})\\f_{i,j-1,0}+(a_j-a_i)*(sum_{i-1}+sum_n-sum_{j-1}))\end{cases}\)

枚举此时所在路灯 \(l\),再进行状态转移,便可求出答案。最终答案为 \(\min(f_{1,n,0},f_{1,n,1})\)

code:

点击查看代码
#include <bits/stdc++.h>
#define N 60
using namespace std;

int n, m, a[N], b[N], sum[N], f[N][N][2];

inline int read(){
	int s = 0, w = 1;
	char ch = getchar();
	for (; ch < '0' || ch > '9'; w *= ch == '-' ? -1 : 1, ch = getchar());
	for (; ch >= '0' && ch <= '9'; s = s * 10 + ch - '0', ch = getchar());
	return s * w;
}

int main(){
	n = read(), m = read();
	for (int i = 1; i <= n; ++i) a[i] = read(), b[i] = read(), sum[i] = sum[i - 1] + b[i];
	memset(f, 0x3f, sizeof(f));
	f[m][m][0] = f[m][m][1] = 0;
	for (int l = 2; l <= n; ++l)//枚举所在路灯 
		for (int i = 1, j = l + i - 1; j <= n; ++i, ++j){//枚举左端点i和右端点j 
			f[i][j][0] = min(f[i + 1][j][0] + (a[i + 1] - a[i]) * (sum[n] + sum[i] - sum[j]), f[i + 1][j][1] + (a[j] - a[i]) * (sum[n] + sum[i] - sum[j]));//继续往前走or回头 
			f[i][j][1] = min(f[i][j - 1][1] + (a[j] - a[j - 1]) * (sum[n] + sum[i - 1] - sum[j - 1]), f[i][j - 1][0] + (a[j] - a[i]) * (sum[n] + sum[i - 1] - sum[j - 1]));//回头or继续往前走 
		}
	printf("%d\n", min(f[1][n][0], f[1][n][1]));//取最小值 
	return 0;
}

3.环形dp

1.环形最大子段和

题意:

分析:

首先,我们要引入单调队列的概念。单调队列,是在区间内维护最值的算法。例题:滑动窗口

点击查看代码
#include <bits/stdc++.h>
#define N 1000010
using namespace std;

int n, k, a[N], q[N], p[N];

inline int read(){
	int s = 0, w = 1;
	char ch = getchar();
	for (; ch < '0' || ch > '9'; w *= ch == '-' ? -1 : 1, ch = getchar());
	for (; ch >= '0' && ch <= '9'; s = s * 10 + ch - '0', ch = getchar());
	return s * w;
}

void minn(){
	int head = 1, tail = 0;
	for (int i = 1; i <= n; ++i){
		while (head <= tail && q[tail] >= a[i]) tail--;
		q[++tail] = a[i], p[tail] = i;
		while (p[head] <= i - k) head++;
		if (i >= k) printf("%d ", q[head]);
	}
	puts("");
}

void maxn(){
	int head = 1, tail = 0;
	for (int i = 1; i <= n; ++i){
		while (head <= tail && q[tail] <= a[i]) tail--;
		q[++tail] = a[i], p[tail] = i;
		while (p[head] <= i - k) head++;
		if (i >= k) printf("%d ", q[head]);
	}
	puts("");
}

int main(){
	n = read(), k = read();
	for (int i = 1; i <= n; ++i) a[i] = read();
	minn(), maxn();
	return 0;
}

code:


4.树形dp

1.最大子树和

题意:

在一张图上,有 \(n\)个点,每个点有一个点权 \(a_i\),并有 \(n-1\)条边分别连接 \(2\)个节点。求这个图上的最大联通点权和。

分析:

\(n\)\(n-1\)边便可推断其是一棵树,再由点权及求最值便可联系到树形dp。设 \(f_i\)为以 \(i\)为根的子树(包括 \(i\))的最大点权和,通过遍历整棵树进行求解。最终答案为 \(\max\limits_{1\le i\le n}(f_i)\)

code:

点击查看代码
#include <bits/stdc++.h>
#define N 160010
using namespace std;

int n, ans = INT_MIN, cnt, a[N], f[N], head[N];

struct xcj{
	int to, nxt;
} e[N];

inline int read(){
	int s = 0, w = 1;
	char ch = getchar();
	for (; ch < '0' || ch > '9'; w *= ch == '-' ? -1 : 1, ch = getchar());
	for (; ch >= '0' && ch <= '9'; s = s * 10 + ch - '0', ch = getchar());
	return s * w;
}

void add(int u, int v){e[++cnt] = {v, head[u]}, head[u] = cnt;}//链式前向星存图 

void dp(int x, int fa){
	f[x] = a[x];
	for (int i = head[x]; i; i = e[i].nxt){//链式前向星遍历 
		int y = e[i].to;
		if (y != fa){
			dp(y, x);
			if (f[y] > 0) f[x] += f[y];//不断更新子树的权和 
		}
	}
}

int main(){
	n = read();
	for (int i = 1; i <= n; ++i) a[i] = read();
	for (int i = 1; i < n; ++i){
		int u = read(), v = read();
		add(u, v), add(v, u);
	}
	dp(1, -1);
	for (int i = 1; i <= n; ++i) ans = max(ans, f[i]);
	printf("%d\n", ans);
	return 0;
}

2.[NOI1999] 最优联通子集

题意:

在一张图上有 \(n\)个点,每个点有自己的坐标 \(x,y\)及点权z。求联通子集的最大权和。

分析:

本题只需在上一题的基础上增加连边建图的环节即可。由题意可知,若两点的坐标 \(x_1,y_1\)\(x_2,y_2\)满足 \(\left|x_1-x_2\right|+\left|y_1-y_2\right|=1\),则两点相邻。我们便可以根据此定义连边建图。

code:

点击查看代码
#include <bits/stdc++.h>
#define N 100010
using namespace std;

int n, ans, cnt, f[N], head[N];

struct xcj{
	int to, nxt;
} e[N];

struct xcx{
	int x, y, z;
} a[N];

inline int read(){
	int s = 0, w = 1;
	char ch = getchar();
	for (; ch < '0' || ch > '9'; w *= ch == '-' ? -1 : 1, ch = getchar());
	for (; ch >= '0' && ch <= '9'; s = s * 10 + ch - '0', ch = getchar());
	return s * w;
}

void add(int u, int v){e[++cnt] = {v, head[u]}, head[u] = cnt;}

void dp(int u, int fa){//此遍历部分与最大子树和完全一样 
	f[u] = a[u].z;
	for (int i = head[u]; i; i = e[i].nxt){
		int v = e[i].to;
		if (v != fa){
			dp(v, u);
			if (f[v] > 0) f[u] += f[v];
		}
	}
}

int main(){
	n = read();
	for (int i = 1; i <= n; ++i) a[i].x = read(), a[i].y = read(), a[i].z = read();
	for (int i = 1; i < n; ++i)
		for (int j = i + 1; j <= n; ++j)
			if (abs(a[i].x - a[j].x) + abs(a[i].y - a[j].y) == 1) add(i, j), add(j, i);//连边建图 
	dp(1, -1);
	for (int i = 1; i <= n; ++i) ans = max(ans, f[i]);
	printf("%d\n", ans);
	return 0;
}

3.[JSOI2016]最佳团体

题意:

分析:

由题意,设 \(cost\)为招募费用,\(value\)为战斗值 ,则要求 \(\dfrac{\sum cost_i}{\sum value_i}\)最大。因此,可以通过二分求得这个值。所以设 \(\dfrac{\sum cost_i}{\sum value_i}\ge mid\\\therefore\sum cost_i\ge mid*\sum value_i\\\therefore\sum(cost_i-mid*value_i)\ge0\)

\(f_{i,j}\)表示以 \(i\)为根的子树,选取 \(j\)个节点。转移方程为 \(f_{x,j}=max(f_{x,j},f_{x,j-k}+f_{y,k})\),其中 \(j,k\)为循环变量,\(x\)为当前遍历的节点,\(y\)\(x\)的子节点。此外,再维护每个子树的大小即可。

code:

点击查看代码
#include <bits/stdc++.h>
#define N 2510
using namespace std;

int m, n, cnt, s[N], head[N];//s数组记录每个子树的大小 
double res[N], f[N][N];

struct xcj{
	double cost, value;
} a[N];

struct xcx{
	int to, nxt;
} e[N];

inline int read(){
	int s = 0, w = 1;
	char ch = getchar();
	for (; ch < '0' || ch > '9'; w *= ch == '-' ? -1 : 1, ch = getchar());
	for (; ch >= '0' && ch <= '9'; s = s * 10 + ch - '0', ch = getchar());
	return s * w;
}

void add(int x, int y){e[++cnt] = {y, head[x]}, head[x] = cnt;}//链式前向星建图 

void dp(int x, int fa){
	s[x] = 1, f[x][1] = res[x];
	for (register int i = head[x]; i; i = e[i].nxt){
		int y = e[i].to;
		if (y != fa){
			dp(y, x), s[x] += s[y];
			for (register int j = min(m, s[x]); j > 0; --j)
				for (register int k = 0; k <= min(j - 1, s[y]); ++k) f[x][j] = max(f[x][j], f[x][j - k] + f[y][k]);
		}
	}
}

bool chck(double mid){
	res[0] = 0;
	for (register int i = 1; i <= n; ++i) res[i] = a[i].value - mid * a[i].cost;//重置res数组,检验答案是否合法 
	for (register int i = 0; i <= n; ++i)
		for (register int j = 1; j <= m; ++j) f[i][j] = -1e9;
	dp(0, -1);
	return f[0][m] >= 0;
}

int main(){
	m = read() + 1, n = read();
	for (register int i = 1; i <= n; ++i) a[i].cost = read(), a[i].value = read(), add(read(), i);
	double l = 0, r = 10000;
	while (l < r - 1e-4){//二分求答案 
		double mid = (l + r) / 2;
		if (chck(mid)) l = mid;
		else r = mid;
	}
	printf("%.3lf\n", l);
	return 0;
}

5.状压dp

1.吃奶酪

题意:

一张图上有 \(n\)个点,每个点的坐标是 \(x_i,y_i\)。求从点 \((0,0)\)出发,经过所有点的最小距离。

分析:

\(f_{i,j}\)表示从原点到点 \(i\),中间经过点的情况为 \(j\)的二进制的最小距离。首先预处理出每个点之间的距离(两点间距离公式:\(dis(u,v)=\sqrt{(x_u-x_v)^2+(y_u-y_v)^2}\)),再枚举所有可能的情况并更新 \(f\)数组。

转移方程:\(f_{i,k}=\min(f_{i,k},f_{j,k-2^{i-1}}+a_{i,j})\),其中 \(1\le i,j\le n,1\le k\le2^n\)\(a_{i,j}\)为预处理的两点距离。

code:

点击查看代码
#include <bits/stdc++.h>
using namespace std;

int n;
double ans, x[20], y[20], a[20][20], f[16][1 << 15];

inline int read(){
	int s = 0, w = 1;
	char ch = getchar();
	for (; ch < '0' || ch > '9'; w *= ch == '-' ? -1 : 1, ch = getchar());
	for (; ch >= '0' && ch <= '9'; s = s * 10 + ch - '0', ch = getchar());
	return s * w;
}

double dis(int u, int v){return sqrt((x[u] - x[v]) * (x[u] - x[v]) + (y[u] -y[v]) * (y[u] - y[v]));}//计算距离 

int main(){
	memset(f, 127, sizeof(f)), ans = f[0][0], x[0] = y[0] = 0;//预处理赋值无穷大 
	n = read();
	for (int i = 1; i <= n; ++i) scanf("%lf%lf", &x[i], &y[i]);
	for (int i = 0; i < n; ++i)
		for (int j = i + 1; j <= n; ++j) a[i][j] = a[j][i] = dis(i, j);//预处理任意两点距离 
	for (int i = 1; i <= n; ++i) f[i][1 << (i - 1)] = a[i][0];//预处理从0到i不经过其它点所经过的距离 
	for (int k = 1; k < (1 << n); ++k)//枚举所有情况 
		for (int i = 1; i <= n; ++i){
			if (k & (1 << (i - 1)) == 0) continue;
			for (int j = 1; j <= n; ++j){
				if (i == j || (k & (1 << (j - 1))) == 0) continue;
				f[i][k] = min(f[i][k], f[j][k - (1 << (i - 1))] + a[i][j]);
			}
		}
	for (int i = 1; i <= n; ++i) ans = min(ans, f[i][(1 << n) - 1]);//取min求答案 
	printf("%.2lf\n", ans);
	return 0;
}

2.Mondriaan's Dream

题意:

分析:

code:


posted @ 2022-02-11 14:55  leoair  阅读(68)  评论(0)    收藏  举报