YbtOJ 「动态规划」 第2章 区间DP

区间dp

常见套路:第一维枚举区间长度 第二维枚举左端点计算右端点 第三维枚举\(l,r\)中间的断点(区间开闭依照题目而定 如果需要使用到\(k+1\)那么右端点应该为开区间 否则导致越界)

A. 【例题1】石子合并

设置\(f[i][j]\)表示\(i\)\(j\)合并为一段的最小权值

经典操作:首尾链接成环 成为一个\(2\times n\)的数组 求前缀和实现\(O(1)\)转移 再从\(2\)\(n\)枚举区间长度进行\(dp\) 注意最大值和最小值要分开处理

统计答案时枚举起始点计算区间长度为\(n\)的终止点 统计最大值和最小值

注意数组初值:最小值数组赋值inf 最大值数组赋值-inf \(f[i][i]\)均为0

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 1e3 + 5;
const int inf = 0x3f3f3f3f;
int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , fmaxx[N][N] , fminn[N][N] , a[N] , sum[N];

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read();
	memset ( fminn , inf , sizeof fminn );
	memset ( fmaxx , -inf , sizeof fmaxx );
	for ( int i = 1 ; i <= 2 * n ; i ++ ) fminn[i][i] = fmaxx[i][i] = 0;
	for ( int i = 1 ; i <= n ; i ++ ) a[i+n] = a[i] = read();
	for ( int i = 1 ; i <= 2 * n ; i ++ ) sum[i] = sum[i-1] + a[i];
	for ( int len = 2 ; len <= n ; len ++ )
		for ( int l = 1 , r = l + len - 1 ; l <= 2 * n && r <= 2 * n ; l ++ , r ++ )
			for ( int k = l ; k < r ; k ++ )
			{
				fminn[l][r] = min ( fminn[l][r] , fminn[l][k] + fminn[k+1][r] + sum[r] - sum[l-1] );
				fmaxx[l][r] = max ( fmaxx[l][r] , fmaxx[l][k] + fmaxx[k+1][r] + sum[r] - sum[l-1] ); 
			}
	int ansmaxx = -inf , ansminn = inf;
	for ( int i = 1 ; i <= n ; i ++ ) ansmaxx = max ( ansmaxx , fmaxx[i][i+n-1] ) , ansminn = min ( ansminn , fminn[i][i+n-1] );
	cout << ansminn << endl << ansmaxx;
	return 0;
}

B. 【例题2】木板涂色

同上 设置\(f[l][r]\)表示\(l\)\(r\)区间内涂成目标颜色所需要的次数

如果这一次的左端点和右端点颜色相同 那么这个区间次数可以减一

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 1e3 + 5;
const int inf = 0x3f3f3f3f;
int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

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

string s;

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	cin >> s; n = s.size() , s = " " + s;
	memset ( f , inf , sizeof f );
	for ( int i = 1 ; i <= n ; i ++ ) f[i][i] = 1;
	for ( int len = 2 ; len <= n ; len ++ )
		for ( int l = 1 , r = l + len - 1 ; l <= n && r <= n ; l ++ , r ++ )
		{
			for ( int k = l ; k < r ; k ++ )
				f[l][r] = min ( f[l][r] , f[l][k] + f[k+1][r] ); 
			if ( s[l] == s[r] ) f[l][r] --;
		}
	cout << f[1][n];
	return 0;
}

C. 【例题3】消除木块

一道神题 \(luogu\)原题方块消除 Blocks

首先一定是区间\(dp\) 那么最先想到的状态一定是\(dp[i][j]\)表示消除\([i,j]\)内的所有方块的最大分数 但是这个状态会受到外面与\(i,j\)颜色相同的块的影响

所以考虑换一个状态:\(dp[i][j][k]\)表示消掉区间\([i,j]\)并且区间\([i,j]\)右面还有\(k\)个和\(j\)颜色相同的块所获得的最大分数

也就是当前处理的子问题\(dp[i][j]\)主体由区间\([i,j]\)组成,然后与\(j\)相同有\(k\)块接在后面,这\(k\)块之间的其他块已经全部消完了

优化:我们将相同颜色合并为一段 处理段的个数和段长即可

转移:两种转移

  1. 消掉\(j\)和后面的\(k\)

    dp[i][j][k]=max(dp[i][j][k],dfs(i,j-1,0)+(k+1)*(k+1));
    
  2. 对于区间\([i,j]\) 中间可能有和\(j\)颜色相同的块 假设位置为\(p\) 我们可以选择消掉区间\(p+1,j-1\) 再将两边合并 权值取最大值

    for ( int i = l ; i < r ; i ++ )
    	if ( a[i] == a[r] )
    		f[l][r][k] = max ( f[l][r][k] , dfs ( i + 1 , r - 1 , 0 ) + dfs ( l , i , len[r] + k ) ); 
    
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long 
const int N = 200 + 5;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , a[N] , len[N] , f[N][N][N] , cnt , cases , lst;

int dfs ( int l , int r , int k )
{
	if ( f[l][r][k] ) return f[l][r][k];
	if ( l == r ) return ( len[l] + k ) * ( len[l] + k );
	f[l][r][k] = dfs ( l , r - 1 , 0 ) + ( len[r] + k ) * ( len[r] + k );
	for ( int i = l ; i < r ; i ++ )
		if ( a[i] == a[r] )
			f[l][r][k] = max ( f[l][r][k] , dfs ( i + 1 , r - 1 , 0 ) + dfs ( l , i , len[r] + k ) ); 
	return f[l][r][k];
}

void init()
{
	memset ( f , 0 , sizeof f );
	memset ( a , 0 , sizeof a );
	memset ( len , 0 , sizeof len );
	lst = 0 , cnt = 0;
}


signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	int T = read();
	while ( T -- )
	{	
		init();
		n = read();
		for ( int i = 1 ; i <= n ; i ++ )
		{
			int temp = read();
			if ( temp != lst ) a[++cnt] = temp;
			len[cnt] ++;
			lst = temp;
		}
		cout << "Case " << ++cases << ": " << dfs ( 1 , cnt , 0 ) << endl;
	}
	return 0;
}

D. 【例题4】棋盘分割

\(\sigma=\sqrt{\dfrac{\sum\limits_{i=1}^{n}(x_i-\bar{x})^2}{n}}\) 最小 那么我们考虑让方差\(\sigma^2=\dfrac{\sum\limits_{i=1}^{n}(x_i-\bar{x})^2}{n}\)最小

\(\bar{x}\)是固定的 为整个图的平均值 可以预处理

方差柿子:

\[\begin{aligned} &=\frac{\sum_{i=1}^n(x_i-\overline{x})^2}{n}\\ &=\frac{1}{n}\cdot \sum_{i=1}^n(x_i^2-2x_i\overline{x}+\overline{x}^2)\\ &=\frac{1}{n}\cdot(\sum_{i=1}^nx_i^2-2\overline{x}\sum_{i=1}^nx_i+n\overline{x}^2)\\ &=\frac{1}{n}\cdot(\sum_{i=1}^nx_i^2-2\overline{x}\cdot n\overline{x}+n\overline{x}^2)\\ &=\frac{1}{n}\cdot(\sum_{i=1}^nx_i^2-n\overline{x}^2)\\ &=\frac{\sum_{i=1}^nx_i^2}{n}-\overline{x}^2\\ \end{aligned}\]

我们现在设置\(f[x1][y1][x2][y2][k]\)表示以\((x1,y1)\)为左上角 \((x2,y2)\)为右下角分成\(k\)块棋盘的方差 然后维护

#include <bits/stdc++.h>
using namespace std;
#define int long long

const int inf = 2e18;
const int M = 20;
double f[M][M][M][M][M],ave;
int sum[M][M] , n;
inline double calc ( int x1 , int y1 , int x2 , int y2 )
{
	double res = sum[x2][y2] - sum[x2][y1-1] - sum[x1-1][y2] + sum[x1-1][y1-1];
	return res * res / n;
}
signed main(){
	scanf ( "%lld" , &n ); 
	for ( int i = 1 ; i <= 8 ; i ++ )
		for ( int j = 1 ; j <= 8 ; j ++ )
		{ scanf ( "%lld" , &sum[i][j] ); sum[i][j] += sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1]; }
	ave = (double)sum[8][8] / n;
	for ( int k = 1 ; k <= n ; k ++ )//分割几块小矩形 
		for ( int x1 = 1 ; x1 <= 8 ; x1 ++ )
			for ( int y1 = 1 ; y1 <= 8 ; y1 ++ )
				for ( int x2 = 1 ; x2 <= 8 ; x2 ++ )
					for ( int y2 = 1 ; y2 <= 8 ; y2 ++ )
					{
						f[x1][y1][x2][y2][k] = inf;
						if ( k == 1 ) { f[x1][y1][x2][y2][k] = calc ( x1 , y1 , x2 , y2 ) ; continue; }
						for ( int i = x1 ; i < x2 ; i ++ )
						{
							f[x1][y1][x2][y2][k] = min ( f[x1][y1][x2][y2][k] , f[x1][y1][i][y2][k-1] + calc ( i + 1 , y1 , x2 , y2 ) ); 
							f[x1][y1][x2][y2][k] = min ( f[x1][y1][x2][y2][k] , f[i+1][y1][x2][y2][k-1] + calc ( x1 , y1 , i , y2 ) );
							//把矩形分成了左右两个矩形,进行转移 
						}
						for ( int i = y1 ; i < y2 ; i ++ )
						{
							f[x1][y1][x2][y2][k] = min ( f[x1][y1][x2][y2][k] , f[x1][y1][x2][i][k-1] + calc ( x1 , i + 1 , x2 , y2 ) ); 
							f[x1][y1][x2][y2][k] = min ( f[x1][y1][x2][y2][k] , f[x1][i+1][x2][y2][k-1] + calc ( x1 , y1 , x2 , i ) );
							//把矩形分成了上下两个矩形,进行转移 
						} 
					}
	printf ( "%.3lf" , sqrt ( f[1][1][8][8][n] - ave * ave ) ); 
	return 0;
}

E. 1.删数问题

我们设置\(f[l][r]\)表示删掉\([l,r]\)区间内的数所获得的最大价值

那么转移就是$f[l][r] = max ( f[l][r] , max ( f[l][k] + value ( k + 1 , r ) , f[k+1][r] + value ( l , k ) ) ); $ 其中\(value(l,r)\)表示这一段区间内的价值

需要注意:枚举断点的右端点要取等 因为可能一次删除区间内的所有数是最优的

#include <bits/stdc++.h>
using namespace std;
#define inl inline
const int N = 1e3 + 5;
inl int read ()
{
	int x = 0 , f = 1;
	char ch = getchar();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
	return x * f;
}


int a[N] , f[N][N] , n;

int value ( int l , int r )
{
	if ( l > r ) return 0;
	if ( l == r ) return a[l];
	return abs ( a[r] - a[l] ) * ( r - l + 1 );	
}

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read();
	for ( int i = 1 ; i <= n ; i ++ ) a[i] = read() , f[i][i] = a[i];
	for ( int len = 2 ; len <= n ; len ++ )
		for ( int l = 1 , r = l + len - 1 ; r <= n ; l ++ , r ++ )
			for ( int k = l ; k <= r ; k ++ )
				f[l][r] = max ( f[l][r] , max ( f[l][k] + value ( k + 1 , r ) , f[k+1][r] + value ( l , k ) ) );  
	cout << f[1][n] << endl;
	return 0;
}

F. 2.恐狼后卫

\(1h\)调过的题 坑点很多

首先我们对于每一只狼 必须将它完全杀死再找其他狼 否则一定不优

那么我们设置\(f[l][r]\)表示\([l,r]\)区间内所有狼杀死的最大价值

转移式为\(f[l][r] = min ( f[l][r] , f[l][k-1] + f[k+1][r] + ( a[l-1].b + a[k].a + a[r+1].b ) * a[k].cnt );\)

意思为将\([l,k-1]\)\([k+1,r]\)都消掉 并将左面的\(l-1\)\(r+1\)\(k\)相邻进行转移

其中\(a[i].cnt\)为将这个狼杀死的最小时间

区间\(dp\)枚举即可

坑点1:不能\(memset\)整个数组为\(inf\)因为在使用的时候需要使用\(f[1][0]\)\(f[1][0]\)表示的是没有狼的情况 所以\(f[1][0]=0\)而不能赋值\(inf\)

坑点2:每一个狼的杀死时间为\(a[i].cnt = ceil((double)(a[i].h)/atk)\) 注意必须为\(double\)

#include <bits/stdc++.h>
using namespace std;
#define inl inline

#define int long long 

const int inf = 0x3f3f3f3f3f3f3f3f;
const int N = 1e3 + 5;
inl int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}


int atk , n , f[N][N];

struct node { int a , b , h , cnt; } a[N];

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , atk = read();
	for ( int i = 0 ; i <= n ; i ++ )
		for ( int j = 0 ; j <= n ; j ++ )
			if ( i <= j ) f[i][j] = inf;
	for ( int i = 1 ; i <= n ; i ++ ) a[i].a = read() , a[i].b = read() , a[i].h = read() , a[i].cnt = ceil((double)(a[i].h)/atk);
	for ( int i = 1 ; i <= n ; i ++ ) f[i][i] = ( a[i-1].b + a[i].a + a[i+1].b ) * a[i].cnt;
	for ( int len = 2 ; len <= n ; len ++ )
		for ( int l = 1 , r = l + len - 1 ; r <= n ; l ++ , r ++ )
			for ( int k = l ; k <= r ; k ++ )
				f[l][r] = min ( f[l][r] , f[l][k-1] + f[k+1][r] + ( a[l-1].b + a[k].a + a[r+1].b ) * a[k].cnt );
	cout << f[1][n] << endl;
	return 0;
}

G. 3.矩阵取数

我们设置\(f[i][j]\)表示这一行从左面删了\(i\)个 右面删了\(j\)个所能获得的最大价值

所以可以预处理从右面删除\(1-n\)个的值 从左面删除\(1-n\)个的值

\(f[0][i] = f[0][i-1] + a[m-i+1] * base[i]\)

\(f[i][0] = f[i-1][0] + a[i] * base[i];\)

那么我们枚举所有端点 \(2^{i+j}*a[i]\)即为当前删除的价值

\(f[i][j] = max ( f[i-1][j] + base[i+j] * a[i] , f[i][j-1] + base[i+j] * a[m-j+1] )\)

最后统计的答案是这一行中所有左面删数+右面删数个数等于\(m\)的价值之和的最大值

需要快写+快读+__int128来过掉此题

#include <bits/stdc++.h>
using namespace std;
#define inl inline
#define int __int128
const int N = 1e3 + 5;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

void print ( int x )
{
	if ( x < 0 ) putchar ( '-' ) , x = -x;
	if ( x > 9 ) print ( x / 10 );
	putchar ( x % 10 + '0' );
}

int n , m , f[N][N] , ans , a[N] , base[N] = {1};

signed main ()
{
	n = read() , m = read();
	for ( int i = 1 ; i <= n + m ; i ++ ) base[i] = base[i-1] * 2;
	for ( int k = 1 ; k <= n ; k ++ )
	{
		for ( int i = 1 ; i <= m ; i ++ ) a[i] = read();
		memset ( f , 0 , sizeof f );
		int maxx = 0;
		for ( int i = 1 ; i <= m ; i ++ ) f[0][i] = f[0][i-1] + a[m-i+1] * base[i] , f[i][0] = f[i-1][0] + a[i] * base[i];
		for ( int i = 1 ; i <= m ; i ++ )
			for ( int j = 1 ; j <= m ; j ++ ) 
				if ( i + j <= m )
					f[i][j] = max ( f[i-1][j] + base[i+j] * a[i] , f[i][j-1] + base[i+j] * a[m-j+1] );
		for ( int i = 0 ; i <= m ; i ++ ) maxx = max ( f[i][m-i] , maxx );
		ans += maxx;
	}
	print(ans);
	return 0;
}

H. 4.生日欢唱

我们设置设\(f[i][j]\)表示前\(i\)个男生和前\(j\)个女生 且第\(i\)个男生和第\(j\)个女生强制配对的最大欢乐值

所以对于每一个状态 它可以从两个前前置状态转移过来

\(f_{i,j}=\max\limits_{1\leq k\leq j-1}\{f_{i-1,k}+a_i\times b_j+(\sum\limits_{s=k+1}^{j-1}b_s)^2\}\)

\(f_{i,j}=\max\limits_{1\leq k\leq i-1}\{f_{k,j-1}+a_i\times b_j+(\sum\limits_{s=k+1}^{j-1}b_s)^2\}\)

统计答案:我们可以在男生和女生的最后都加一个\(0\)节点 最后\(f[n+1][n+1]\)就是答案

#include <bits/stdc++.h>
using namespace std;
#define inl inline
#define int long long 
const int N = 1e3 + 5;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , sa[N] , sb[N] , a[N] , b[N] , f[N][N];


signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read();
	for ( int i = 1 ; i <= n ; i ++ ) a[i] = read() , sa[i] = sa[i-1] + a[i];
	for ( int i = 1 ; i <= n ; i ++ ) b[i] = read() , sb[i] = sb[i-1] + b[i];
	sa[n+1] = sa[n] , sb[n+1] = sb[n];
	for ( int i = 1 ; i <= n + 1 ; i ++ )
	{
		f[1][i] = a[1] * b[i] - sb[i-1] * sb[i-1];
		f[i][1] = a[i] * b[1] - sa[i-1] * sa[i-1];
	}
	for ( int i = 2 ; i <= n + 1 ; i ++ )
		for ( int j = 2 ; j <= n + 1 ; j ++ )
		{
			for ( int k = 1 ; k < i ; k ++ )
				f[i][j] = max ( f[i][j] , f[k][j-1] + a[i] * b[j] - (int)pow ( sa[i-1] - sa[k] , 2 ) );
			for ( int k = 1 ; k < j ; k ++ )
				f[i][j] = max ( f[i][j] , f[i-1][k] + a[i] * b[j] - (int)pow ( sb[j-1] - sb[k] , 2 ) );
		}
	cout << f[n+1][n+1] << endl;
	return 0;
}

I. 5.最小代价

神题 留坑待填(可能不会去填了)

posted @ 2023-06-30 12:17  Echo_Long  阅读(115)  评论(0编辑  收藏  举报