市面上的区间DP,大多都是从石子合并(链式)、石子合并(环式)开始讲起,但是笔者认为他们夹杂着前缀和,对初学者很不友好。所以我打算用另一题来引出。
给定一个字符串,求出将给定字符串变成回文词所需要插入的最少字符数。
其实经过线性DP的折磨后,大家也应该明白DP,应该由状态定义 、边界 、状态转移 几个方面组成,下面就理清这三个东西。估计对于区间dp也应该会有一个理性的认识。
/*
状态:
回文词所需要插入的最少字符数,是关于区间长度的函数
dp[i][j] —— 在区间从i到j,回文词所需要插入的最少字符数
边界:
区间长度为1(len == 1) —— 必是回文字串,dp[i][i] = 0;
状态转移:
if (s[l] == s[r]) dp[l][r] = dp[l + 1][r - 1];//s[l]和s[r]相等,不需要插入
else dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]) + 1;//最少字符数,只跟上一个小区间有关,要么是左边的,要么是右边
递推的写法:
从小区间到大区间。
*/
#include
using namespace std;
#define ll long long
#define ull unsigned long long
#define P pair<int, int>
#define endl '\n'
#define MaxN 0x3f3f3f3f
#define MinN -MaxN
const int mod = 1e9 + 7;
string s;
void slove(){
cin >> s;
int n = s.size();
s = " " + s;//下标从1开始
vector<vector<int>> dp(n + 7, vector<int> (n + 7, 0));
for (int len = 1; len <= n; len++){//区间长度
for (int l = 1; l + len - 1 <= n; l++){//枚举,区间范围
int r = l + len - 1;
if (s[l] == s[r]) dp[l][r] = dp[l + 1][r - 1];//不需要插入
else dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]) + 1;//从左边的区间或者右边的区间转移不过来,需要插入
}
}
cout << dp[1][n];//1 ~ n 范围内的最小插入
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
// cin >> t;
while (t--){
slove();
}
return 0;
}
本来是现在把石子合并放到这讲的,但是发现了另一个好题。能诠释==(割点k——大区间分成两部分)== ,也不用再讲前缀和增加理解负担。
把一段连续的木板涂成一个给定的颜色,求解最少的涂色次数
/*
状态:
最少的涂色次数。关于区间的函数
dp[l][r]
边界:
区间长度 len == 1、l == r 时。只需涂一次色, dp[i][i] = 1
求最少的涂色次数 其他初始化为MaxN
状态转移:
状态转移:
s[i] == s[j]
dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]);//过大上次涂的边界即可,不需要再涂
else
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]);// 我们需要考虑将子串断成两部分来涂色,需要枚举子串的断点
*/
#include
using namespace std;
#define ll long long
#define ull unsigned long long
#define P pair<int, int>
#define endl '\n'
#define MaxN 0x3f3f3f3f
#define MinN -MaxN
const int mod = 1e9 + 7;
void slove(){
string s;
cin >> s;
int n = s.size();
s = " " + s;
vector<vector<int >> dp(n + 7, vector<int>(n + 7, MaxN));//初始化
for (int i = 1; i <= n; i++) dp[i][i] = 1;//边界
for (int len = 2; len <= n; len++){//len从2开始,1已经遍历过了
for (int l = 1; l + len - 1 <= n; l++){
int r = l + len - 1;
if (s[l] == s[r]) dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]);
else for (int k = l; k < r; k++) dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]);
}
}
cout << dp[1][n];
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
// cin >> t;
while (t--){
slove();
}
return 0;
}
终于到激动人心的时候了!石子合并!这里提一嘴,石子合并有链式和环式,现在先将链式。
合并的代价为这两堆石子的质量之和,求总的代价最小
/*
状态:
总的代价最小,关于区间的函数
dp[i][j]
边界:
dp[i][i] = 0//不合并,没有代价
求总的代价最小 其他初始化为MaxN
状态转移:
两堆原先的重量,再加合并的重量
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + s[r] - s[l - 1]);
*/
#include
using namespace std;
#define ll long long
#define ull unsigned long long
#define P pair<int, int>
#define endl '\n'
const int mod = 1e9 + 7;
const int maxl = 3 * 1e2 + 7;
int n;
void slove(){
cin >> n;
vector<int> a(n + 1, 0), s(n + 1, 0);//a ——存数, s ——前缀和
vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0x3f3f3f3f));
for (int i = 1; i <= n; i++){//从下标1开始
cin >> a[i];
s[i] = s[i - 1] + a[i];
dp[i][i] = 0;
}
for (int len = 2; len <= n; len++){//枚举区间长度
for (int l = 1; l + len - 1 <= n; l++){//移动区间、范围[l,l + len)
int r = l + len - 1;
for (int k = l; k < r; k++){//这个区间分成两个部分,[l, k] ~ [k + 1, r] ,这里是k + 1所以 k < r
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + s[r] - s[l - 1]);
}
}
}
cout << dp[1][n];
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
// cin >> t;
while (t--){
slove();
}
return 0;
}
环形dp,其实不难,就多复制了一维,其他都一样的
合并的代价为这两堆石子的质量之和,求总的代价最小
首尾可以相连成环
#include
using namespace std;
#define ll long long
#define ull unsigned long long
#define P pair<int, int>
#define endl '\n'
#define MaxN 0x3f3f3f3f
#define MinN -MaxN
const int mod = 1e9 + 7;
int n;
void slove(){
cin >> n;
vector<vector<int>> dpMax(n * 2 + 7, vector<int> (2 * n + 7, MinN)), dpMin(n * 2 + 1, vector<int> (2 * n + 1, MaxN));
vector<int> s(2 * n + 7, 0), a(2 * n + 7, 0);
for (int i = 1; i <= n; i++){
cin >> a[i];
a[i + n] = a[i];
}
for (int i = 1; i <= 2 * n; i++){
s[i] = s[i - 1] + a[i];
dpMax[i][i] = 0;
dpMin[i][i] = 0;
}
for (int len = 2; len <= n; len++){
for (int l = 1; l + len - 1 <= 2 * n; l++){
int r = l + len - 1;
for (int k = l; k < r; k++){
dpMax[l][r] = max(dpMax[l][r], dpMax[l][k] + dpMax[k + 1][r] + s[r] - s[l - 1]);
dpMin[l][r] = min(dpMin[l][r], dpMin[l][k] + dpMin[k + 1][r] + s[r] - s[l - 1]);
}
}
}
int maxAns = MinN, minAns = MaxN;
for (int i = 1; i <= n; i++){
minAns = min(minAns, dpMin[i][i + n - 1]);
maxAns = max(maxAns, dpMax[i][i + n - 1]);
// cout << maxAns << endl;
}
cout << minAns << endl << maxAns << endl;
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
// cin >> t;
while (t--){
slove();
}
return 0;
}
前缀和,区间长度为n,但是如果不是前缀和,区间长度就要变成n + 1
和石子合并差不多,只不过那个是相加,这个是相乘
和石子合并一个思路不多赘述,唯一一个点就是不用前缀和,首尾相连的话,区间范围为n + 1,包含首尾相连
#include
using namespace std;
#define ll long long
#define ull unsigned long long
#define P pair<int, int>
#define endl '\n'
#define MaxN 0x3f3f3f3f
#define MinN -MaxN
const int mod = 1e9 + 7;
int n, ans = 0;
void slove(){
cin >> n;
vector<int> a(2 * n + 7, 0);
vector<vector<int>> dp(2 * n + 7, vector<int>(2 * n + 7, 0));
for (int i = 1; i <= n; i++) cin >> a[i], a[i + n] = a[i];
for (int len = 3; len <= n + 1; len++){
for (int l = 1; l + len - 1 <= 2 * n; l++){
int r = l + len - 1;
for (int k = l +1; k < r; k++) dp[l][r] = max(dp[l][r], dp[l][k] + dp[k][r] + a[l] * a[k] * a[r]);
}
}
for (int i = 1; i <= n; i++) ans = max(ans, dp[i][i + n]);
cout << ans;
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
// cin >> t;
while (t--){
slove();
}
return 0;
}
完结散花,如有不明白欢迎私信交流。都看到这了,给个三连呗!