一、前序知识
二、环形样例理解
三、问题如何转化
我们是不是可以利用以前学习过的\(AcWing\) \(282\). 石子合并那道题进行扩展来解决呢?因为共\(n\)个节点,那么就是需要合并\(n-1\)次,在图上理解就是
这样,我们枚举\(n-1\)次,即可以把这个环形问题转化为一个经典的区间\(dp\)问题,接下来我们计算一下时间:
原来\(AcWing\) \(282\). 石子合并的时间复杂度是\(O(N^3)\),这里面还需要执行\(n-1\)次,那就时间复杂度就是\((n-1)O(N^3)=O(N^4)\),现在\(N\)的上限是\(200\),\(200^4=1,600,000,000\),很显然会超时,此路不通。
四、优化办法
环形问题的经典优化办法
1、构造一个长度为\(2*n\)的区间,模拟尾首相连。
启发我们在长度是\(2n\)的链上进行一次石子合并问题,预处理出来所有的区间\(f[i][j]\),然后枚举区间长度为\(n\)的所有子区间,就一定能枚举到\(n\)种情况了。\(f[i][j]->f[i][j+n-1]\)
这样的时间复杂度就是\(O((2N)^3)\),然后再通过一次常数的\(O(N)\)就可以搞定了,看清楚,这两个时间复杂度是加法关系,如此,我们就把一个时间复杂度是\(O(N^4)\)的算法,优化为一个\(O((2N)^3)\)级别的算法,\(400^3=64,000,000\)
是可以一秒通过的。
而且这个办法是一个通用办法,是可以把环形问题转化。
五、记忆化搜索代码
#include <bits/stdc++.h>
//记忆化搜索
//洛谷 P1880 [NOI1995] 石子合并
//11个测试点,可以通过10个,第11个TLE
//洛谷可以AC掉
using namespace std;
const int INF = 0x3f3f3f3f;
int n, m, ans1, ans2;
const int N = 210;
int a[N]; //原始数组
int s[N]; //前缀和
int f[N][N], g[N][N];
//搜索出l~r的最小得分
int dfs1(int l, int r) { //求出最小得分
int &v = f[l][r]; //利用C++特性,定义一个v,简化代码
if (l == r) return v = 0; //l==r时返回0
if (v)return v; //已保存的状态不必搜索
int res = INF; //初始值赋为最大值以求最小值
//枚举l~r之间的每个可能k,之所以k∈[l,r-1],可以看下面的递推关系式dfs1(k+1,r)决定了k最大是r-1
for (int k = l; k < r; k++)
res = min(res, dfs1(l, k) + dfs1(k + 1, r) + s[r] - s[l - 1]);
return v = res; //记录状态
}
//搜索出l~r的最大得分
int dfs2(int l, int r) { //求出最大得分
int &v = g[l][r]; //利用C++特性,定义一个v,简化代码
if (l == r) return v = 0; //若初始值为0可省略该句
if (v)return v; //l==r时返回0
int res = 0; //初始值设为0
for (int k = l; k < r; k++)
res = max(res, dfs2(l, k) + dfs2(k + 1, r) + s[r] - s[l - 1]);
return v = res; //记录状态
}
int main() {
//优化输入
ios::sync_with_stdio(false);
cin >> n;
//因为是环所以保存为长度为2*n的链以保证不会漏解
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];
//搜索出1-2n的最小得分
dfs1(1, 2 * n);
//搜索出1-2n的最大得分
dfs2(1, 2 * n);
ans1 = INF;
ans2 = 0;
for (int i = 1; i <= n; i++) {
ans1 = min(f[i][n + i - 1], ans1);//选出最小答案
ans2 = max(g[i][n + i - 1], ans2);//选出最大答案
}
cout << ans1 << endl << ans2;
return 0;
}
六、DP实现代码
#include <bits/stdc++.h>
using namespace std;
//准备两倍的空间
const int N = 410;
const int INF = 0x3f3f3f3f;
int n; //n个节点
int s[N]; //前缀和
int a[N]; //记录每个节点的石子数量
int f[N][N];//区间DP的数组(最大值)
int g[N][N];//区间DP的数组(最小值)
//环形DP:学习通用办法
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
//复制后半段的数组,构建一个长度为2*n的数组,环形DP问题的处理技巧
a[i + n] = a[i];
}
//预处理前缀和
for (int i = 1; i <= n * 2; i++)s[i] = s[i - 1] + a[i];
//预求最小,先放最大
memset(f, 0x3f, sizeof f);
//预求最大,先放最小
memset(g, -0x3f, sizeof g);
//len=1时,代价0
for (int i = 1; i <= 2 * n; i++)f[i][i] = g[i][i] = 0;
//区间DP的迭代式经典写法
for (int len = 2; len <= n; len++) //枚举区间长度
for (int l = 1; l + len - 1 <= n * 2; l++) {//枚举左端点
//计算出右端点
int r = l + len - 1;
//枚举分界点k
for (int k = l; k < r; k++) {
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
}
}
//因为从哪个位置断开环都是可行的,所以,我们依次检查一下
int Min = INF, Max = -INF;
for (int i = 1; i <= n; i++) {
Min = min(Min, f[i][i + n - 1]);
Max = max(Max, g[i][i + n - 1]);
}
//输出
printf("%d\n%d\n", Min, Max);
return 0;
}