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\)块之间的其他块已经全部消完了
优化:我们将相同颜色合并为一段 处理段的个数和段长即可
转移:两种转移
-
消掉\(j\)和后面的\(k\)块
dp[i][j][k]=max(dp[i][j][k],dfs(i,j-1,0)+(k+1)*(k+1));
-
对于区间\([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}\)是固定的 为整个图的平均值 可以预处理
方差柿子:
我们现在设置\(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.最小代价
神题 留坑待填(可能不会去填了)