区间dp
区间dp
1.算法分析
算法思想
使用f[l][r]表示对[l, r]这段区间的处理答案,那么f[l, r]可以划分为对f[l, k]和f[k, r]两段区间的处理答案之和
算法时间复杂度
O(n3)
可以处理的问题分类
- 合并线性区间问题
- 合并环型区间问题
- 多边形划分三角形问题
- 子树划分问题
- 二维分割问题
2. 算法模板
for (枚举区间长度)
for (枚举左端点)
for (枚举分割点)
f[l, r] = f[l, k] + f[k, r] + val(l, r);
3. 典型例题
3.1 线性石子合并问题
acwing282石子合并
设有N堆石子排成一排,其编号为1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这N堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有4堆石子分别为 1 3 5 2, 我们可以先合并1、2堆,代价为4,得到4 5 2, 又合并 1,2堆,代价为9,得到9 2 ,再合并得到11,总代价为4+9+11=24;
如果第二步是先合并2,3堆,则代价为7,得到4 7,最后一次合并代价为11,总代价为4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数N表示石子的堆数N。
第二行N个数,表示每堆石子的质量(均不超过1000)。
输出格式
输出一个整数,表示最小代价。
数据范围
1≤N≤300
输入样例:
4
1 3 5 2
输出样例:
22
/*
状态表示:f[l][r]表示从l~r这段区间的最小值
状态划分:中间节点k的位置
状态转移:f[l][r] = f[l][k] + f[k + 1][r] + s[r] - s[l - 1]
入口: f[i][i] = 0, f[l][r] = INF(l != r)
出口: f[1][n]
*/
#include <bits/stdc++.h>
using namespace std;
int const N = 3e2 + 10, INF = 0x3f3f3f3f;
int f[N][N], n, w[N], s[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> w[i];
s[i] = s[i - 1] + w[i]; // 求前缀和
}
for (int len = 1; len <= n; ++len) // 枚举区间长度
{
for (int l = 1; l + len - 1 <= n; ++l) // 枚举左端点
{
int r = l + len - 1; // 右端点
if (len == 1) f[l][r] = 0; // 区间长度为1时,为入口
else // 区间长度大于1时,表示这段区间需要更新
{
f[l][r] = INF; // 一开始为不合法状态
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]); // 更新
}
}
}
cout << f[1][n] << endl;
return 0;
}
UCF Local Programming Contest 2012(Practice)A.Wall Street Monopoly
题意:
t个样例
给定n座房子,每次可以把相邻的两座房子合并,合并相邻两座房子的花费为 min(第一座房子长,第一座房子宽) * min(第二座房子长, 第二座房子宽) * 100,合并完 形成的新房子长 = 第一座房子长+第二座房子长,宽=max(第一座房子宽,第二座房子宽),求最小的花费。
样例输入:
2 // 样例数目
3 // 3座房子
20 30 60 40 30 50 // 每座房子的长和宽
2
10 90 30 40
输出:
The minimum cost for lot #1 is $200000.
The minimum cost for lot #2 is $30000.
上图答案为200000
/*
区间dp模板题
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 25, INF = 0x3f3f3f3f;
LL f[N][N], a[N], n, weight[N][N], s[N]; // f[l][r]合并第l~第r座房子的最小花费,weight[l][r] = t:第l~r座房子合并后的新房子的宽为t, a[N]:第i座房子的长
int main()
{
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int t;
cin >> t;
while (t--)
{
cin >> n;
// 输入,初始化
for (int i = 1; i <= n; ++i)
{
int t1, t2;
cin >> t1 >> t2;
a[i] = t1;
weight[i][i] = t2;
s[i] = s[i - 1] + t1;
}
for (int len = 1; len <= n; ++len) // 枚举区间长度
{
for (int i = 1; i + len - 1 <= n; ++i) // 枚举左边界
{
int l = i, r = i + len - 1; // 左右边界
if (len == 1)
{
f[l][r] = 0;
}
else
{
f[l][r] = INF;
for (int k = l; k < r; ++k) // 枚举中间节点
{
LL price = min(s[k] - s[l - 1], weight[l][k]) * min(s[r] - s[k], weight[k + 1][r]) * 100;
if (f[l][r] > f[l][k] + f[k + 1][r] + price) // 只有满足这个条件才能去更新weight数组
{
f[l][r] = f[l][k] + f[k + 1][r] + price;
weight[l][r] = max(weight[l][k], weight[k + 1][r]);
}
}
}
}
}
printf("The minimum cost for lot #1 is $%lld.\n\n", f[1][n]);
}
return 0;
}
3.2 环型石子合并问题
环型石子合并只需要把原先的石子合并问题从1~n扩展成1~2n,其中n+1~2n和1~n相同
acwing1068环型石子合并
将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。
规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:
- 选择一种合并石子的方案,使得做 n−1 次合并得分总和最大。
- 选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。
输入格式
第一行包含整数 n,表示共有 n 堆石子。
第二行包含 n 个整数,分别表示每堆石子的数量。
输出格式
输出共两行:
第一行为合并得分总和最小值,
第二行为合并得分总和最大值。
数据范围
1≤n≤200
输入样例:
4
4 5 9 4
输出样例:
43
54
/*
本题在线性石子合并的基础上变成了一个环,转化的技巧是把原先1~n的区域变成1~2n,其中[n+1,2n] = [1,n]
其他的处理方式和原先的一样
求答案的时候需要枚举一下从哪里开始取长度为n的区间
*/
#include <bits/stdc++.h>
using namespace std;
int const N = 2e2 + 10, INF = 0x3f3f3f3f;
int f1[N * 2][N * 2], w[N * 2], s[N * 2], n, f2[N * 2][N * 2]; // f1记录最大值, f2记录最小值
int main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> w[i];
w[i + n] = w[i];
}
for (int i = 1; i <= 2 * n; ++i) s[i] = s[i - 1] + w[i];
// dp更新
for (int len = 1; len <= n; ++len)
{
for (int l = 1; l + len - 1 <= 2 * n; ++l)
{
int r = l + len - 1;
if (len == 1)
{
f1[l][r] = 0;
f2[l][r] = 0;
}
else
{
f1[l][r] = -INF;
f2[l][r] = INF;
for (int k = l; k < r; ++k)
{
f1[l][r] = max(f1[l][r], f1[l][k] + f1[k + 1][r] + s[r] - s[l - 1]);
f2[l][r] = min(f2[l][r], f2[l][k] + f2[k + 1][r] + s[r] - s[l - 1]);
}
}
}
}
// 答案处理,长度区间为n
int maxv = -1, minv = INF;
for (int i = 1; i <= n; ++i)
{
maxv = max(maxv, f1[i][i + n - 1]);
minv = min(minv, f2[i][i + n - 1]);
}
cout << minv << endl << maxv << endl;
return 0;
}
acwing320能力项链
在Mars星球上,每个Mars人都随身佩带着一串能量项链,在项链上有 N 颗能量珠。
能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。
并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。
因为只有这样,通过吸盘(吸盘是Mars人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。
如果前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为 r,尾标记为 n,则聚合后释放的能量为 mrn(Mars单位),新产生的珠子的头标记为 m,尾标记为 n。
需要时,Mars人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。
显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。
例如:设N=4,4颗珠子的头标记与尾标记依次为(2,3) (3,5) (5,10) (10,2)。
我们用记号⊕表示两颗珠子的聚合操作,(j⊕k)表示第 j,k 两颗珠子聚合后所释放的能量。则
第4、1两颗珠子聚合后释放的能量为:(4⊕1)=1023=60。
这一串项链可以得到最优值的一个聚合顺序所释放的总能量为((4⊕1)⊕2)⊕3)= 1023+1035+10510=710。
输入格式
输入的第一行是一个正整数 N,表示项链上珠子的个数。
第二行是N个用空格隔开的正整数,所有的数均不超过1000,第 i 个数为第 i 颗珠子的头标记,当i<N时,第 i 颗珠子的尾标记应该等于第 i+1 颗珠子的头标记,第 N 颗珠子的尾标记应该等于第1颗珠子的头标记。
至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。
输出格式
输出只有一行,是一个正整数 E,为一个最优聚合顺序所释放的总能量。
数据范围
4≤N≤100,
1≤E≤2.1∗109
输入样例:
4
2 3 5 10
输出样例:
710
/*
本题是一种环状的区间dp,因此处理环状的dp可以通过复制[1, n]来实现把环状变成线状的区间dp
转移方程为:f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
*/
#include<bits/stdc++.h>
using namespace std;
int n;
int const N = 2e2 + 10;
int f[N][N], w[N];
int main()
{
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
// 读入
cin >> n;
for (int i = 1; i <= n; ++i)
{
scanf("%d", &w[i]);
w[i + n] = w[i];
}
// 更新状态
for (int len = 3; len <= n + 1; ++len) // 枚举区间长度,因为能够处理到最初的位置,所以是n+1
{
for (int l = 1; l + len - 1 <= 2 * n; ++l) // 左边界
{
int r = l + len - 1;
for (int k = l + 1; k < r; ++k) // 分界点
{
f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]); // 状态转移
}
}
}
// 找答案
int res = 0;
for (int i = 1; i <= n; ++i) res = max(res, f[i][i + n]);
cout << res << endl;
return 0;
}
acwing283多边形
“多边形游戏”是一款单人益智游戏。
游戏开始时,给定玩家一个具有N个顶点N条边(编号1-N)的多边形,如图1所示,其中N = 4。
每个顶点上写有一个整数,每个边上标有一个运算符+(加号)或运算符*(乘号)。
第一步,玩家选择一条边,将它删除。
接下来在进行N-1步,在每一步中,玩家选择一条边,把这条边以及该边连接的两个顶点用一个新的顶点代替,新顶点上的整数值等于删去的两个顶点上的数按照删去的边上标有的符号进行计算得到的结果。
下面是用图1给出的四边形进行游戏的全过程。
最终,游戏仅剩一个顶点,顶点上的数值就是玩家的得分,上图玩家得分为0。
请计算对于给定的N边形,玩家最高能获得多少分,以及第一步有哪些策略可以使玩家获得最高得分。
输入格式
输入包含两行,第一行为整数N。
第二行用来描述多边形所有边上的符号以及所有顶点上的整数,从编号为1的边开始,边、点、边…按顺序描述。
其中描述顶点即为输入顶点上的整数,描述边即为输入边上的符号,其中加号用“t”表示,乘号用“x”表示。
输出格式
输出包含两行,第一行输出最高分数。
在第二行,将满足得到最高分数的情况下,所有的可以在第一步删除的边的编号从小到大输出,数据之间用空格隔开。
数据范围
3≤N≤50,
数据保证无论玩家如何操作,顶点上的数值均在[-32768,32767]之内。
输入样例:
4
t -7 t 4 x 2 x 5
输出样例:
33
1 2
/*
本题是环型区间dp的板子题,f[l][r]表示l点~r点合并成一个点时的最大值,g[l][r]示l点~r点合并成一个点时的最小值
f[l][r] = max(f[l][r], f[l][k] ⊙ f[k +1 ][r])(⊙定义为加法或者乘法)
本题需要注意的是两个最大的数字相乘不一定是最大的数,可能两个最小的数字相乘才是最大的数,因此需要维护一个g[l][r]来记录最小的情况
*/
#include<bits/stdc++.h>
using namespace std;
int const N = 100 + 10, INF = 0x3f3f3f3f;
int f[N][N], point[N], n, g[N][N]; // f[l][r]表示l点~r点合并成一个点时的最大值,g[l][r]示l点~r点合并成一个点时的最小值
char op[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; ++i) // 输入、环型处理
{
cin >> op[i] >> point[i];
point[i + n] = point[i];
op[i + n] = op[i];
}
// 区间dp更新
for (int len = 1; len <= n; ++len)
{
for (int l = 1; l + len - 1 <= 2 * n; ++l)
{
int r = l + len - 1;
if (len == 1)
{
f[l][r] = point[l];
g[l][r] = point[l];
}
else
{
f[l][r] = -INF; // 初始化
g[l][r] = INF;
// 枚举分割点更新
for (int k = l; k < r; ++k)
{
int maxl = f[l][k], maxr = f[k + 1][r];
int minl = g[l][k], minr = g[k + 1][r];
if (op[k + 1] == 't') // 加法的情况
{
f[l][r] = max(f[l][r], maxl + maxr);
g[l][r] = min(g[l][r], minl + minr);
}
else // 乘法的情况
{
int x1 = maxl * maxr, x2 = maxl * minr, x3 = minl * maxr, x4 = minl * minr;
f[l][r] = max(f[l][r], max(max(x1, x2), max(x3, x4)));
g[l][r] = min(g[l][r], min(min(x1, x2), min(x3, x4)));
}
}
}
}
}
int maxv = -INF;
for (int i = 1; i <= n; ++i) maxv = max(maxv, f[i][i + n - 1]);
cout << maxv << endl;
for (int i = 1; i <= n; ++i)
if (f[i][i + n - 1] == maxv) cout << i << " ";
return 0;
}
3.3 多边形划分三角形问题
3.3.1 求面积
把一个n个顶点的多边形划分为n-2个三角形,可以去枚举中间那个三角形的底的左右端点,然后整个多边形的面积值就变成最左边那个多边形+中间那个三角形+最右边那个多边形
acwing1069凸多边形的划分
给定一个具有 N 个顶点的凸多边形,将顶点从 1 至 N 标号,每个顶点的权值都是一个正整数。
将这个凸多边形划分成 N−2 个互不相交的三角形,对于每个三角形,其三个顶点的权值相乘都可得到一个权值乘积,试求所有三角形的顶点权值乘积之和至少为多少。
输入格式
第一行包含整数 N,表示顶点数量。
第二行包含 N 个整数,依次为顶点 1 至顶点 N 的权值。
输出格式
输出仅一行,为所有三角形的顶点权值乘积之和的最小值。
数据范围
N≤50,
数据保证所有顶点的权值都小于109
输入样例:
5
121 122 123 245 231
输出样例:
12214884
/*
f[l][r]表示l点到r点这些点组成的多边形可以划分的三角形的最小面积,那么可以把这个多边形划分为3部分,从l~k这些点组成的多边形,l、k、r三个点组成的三角形,k~r这些点组成的多边形,因此有状态转移方程:f[l][r] = min(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
*/
#include <bits/stdc++.h>
using namespace std;
//大数
struct BigInteger
{
static const int BASE = 100000000; //和WIDTH保持一致
static const int WIDTH = 8; //八位一存储,如修改记得修改输出中的%08d
bool sign; //符号, 0表示负数
size_t length; //位数
vector<int> num; //反序存
//构造函数
BigInteger(long long x = 0) { *this = x; }
BigInteger(const string &x) { *this = x; }
BigInteger(const BigInteger &x) { *this = x; }
//剪掉前导0,并且求一下数的位数
void cutLeadingZero()
{
while (num.back() == 0 && num.size() != 1)
{
num.pop_back();
}
int tmp = num.back(); // tmp是最高位
if (tmp == 0) // 最高位为0,长度为1
{
length = 1;
} // 最高位不为0
else
{
length = (num.size() - 1) * WIDTH;
while (tmp > 0)
{
length++;
tmp /= 10;
}
}
}
//赋值运算符
BigInteger &operator=(long long x)
{
num.clear();
if (x >= 0)
{
sign = true;
}
else
{
sign = false;
x = -x;
}
do
{
num.push_back(x % BASE);
x /= BASE;
} while (x > 0);
cutLeadingZero();
return *this;
}
BigInteger &operator=(const string &str)
{
num.clear();
sign = (str[0] != '-'); //设置符号
int x, len = (str.size() - 1 - (!sign)) / WIDTH + 1;
for (int i = 0; i < len; i++)
{
int End = str.size() - i * WIDTH;
int start = max((int)(!sign), End - WIDTH); //防止越界
sscanf(str.substr(start, End - start).c_str(), "%d", &x);
num.push_back(x);
}
cutLeadingZero();
return *this;
}
BigInteger &operator=(const BigInteger &tmp)
{
num = tmp.num;
sign = tmp.sign;
length = tmp.length;
return *this;
}
//绝对值
BigInteger abs() const
{
BigInteger ans(*this);
ans.sign = true;
return ans;
}
//正号
const BigInteger &operator+() const { return *this; }
//负号
BigInteger operator-() const
{
BigInteger ans(*this);
if (ans != 0)
ans.sign = !ans.sign;
return ans;
}
// + 运算符
BigInteger operator+(const BigInteger &b) const
{
if (!b.sign)
{
return *this - (-b);
}
if (!sign)
{
return b - (-*this);
}
BigInteger ans;
ans.num.clear();
for (int i = 0, g = 0;; i++)
{
if (g == 0 && i >= num.size() && i >= b.num.size())
break;
int x = g;
if (i < num.size())
x += num[i];
if (i < b.num.size())
x += b.num[i];
ans.num.push_back(x % BASE);
g = x / BASE;
}
ans.cutLeadingZero();
return ans;
}
// - 运算符
BigInteger operator-(const BigInteger &b) const
{
if (!b.sign)
{
return *this + (-b);
}
if (!sign)
{
return -((-*this) + b);
}
if (*this < b)
{
return -(b - *this);
}
BigInteger ans;
ans.num.clear();
for (int i = 0, g = 0;; i++)
{
if (g == 0 && i >= num.size() && i >= b.num.size())
break;
int x = g;
g = 0;
if (i < num.size())
x += num[i];
if (i < b.num.size())
x -= b.num[i];
if (x < 0)
{
x += BASE;
g = -1;
}
ans.num.push_back(x);
}
ans.cutLeadingZero();
return ans;
}
// * 运算符
BigInteger operator*(const BigInteger &b) const
{
int lena = num.size(), lenb = b.num.size();
BigInteger ans;
for (int i = 0; i < lena + lenb; i++)
ans.num.push_back(0);
for (int i = 0, g = 0; i < lena; i++)
{
g = 0;
for (int j = 0; j < lenb; j++)
{
long long x = ans.num[i + j];
x += (long long)num[i] * (long long)b.num[j];
ans.num[i + j] = x % BASE;
g = x / BASE;
ans.num[i + j + 1] += g;
}
}
ans.cutLeadingZero();
ans.sign = (ans.length == 1 && ans.num[0] == 0) || (sign == b.sign);
return ans;
}
//*10^n 大数除大数中用到
BigInteger e(size_t n) const
{
int tmp = n % WIDTH;
BigInteger ans;
ans.length = n + 1;
n /= WIDTH;
while (ans.num.size() <= n)
ans.num.push_back(0);
ans.num[n] = 1;
while (tmp--)
ans.num[n] *= 10;
return ans * (*this);
}
// /运算符 (大数除大数)
BigInteger operator/(const BigInteger &b) const
{
BigInteger aa((*this).abs());
BigInteger bb(b.abs());
if (aa < bb)
return 0;
char *str = new char[aa.length + 1];
memset(str, 0, sizeof(char) * (aa.length + 1));
BigInteger tmp;
int lena = aa.length, lenb = bb.length;
for (int i = 0; i <= lena - lenb; i++)
{
tmp = bb.e(lena - lenb - i);
while (aa >= tmp)
{
str[i]++;
aa = aa - tmp;
}
str[i] += '0';
}
BigInteger ans(str);
delete[] str;
ans.sign = (ans == 0 || sign == b.sign);
return ans;
}
// < 运算符
bool operator<(const BigInteger &b) const
{
if (sign != b.sign) //正负,负正
{
return !sign;
}
else if (!sign && !b.sign) //负负
{
return -b < -*this;
}
//正正
if (num.size() != b.num.size())
return num.size() < b.num.size();
for (int i = num.size() - 1; i >= 0; i--)
if (num[i] != b.num[i])
return num[i] < b.num[i];
return false;
}
bool operator>(const BigInteger &b) const { return b < *this; } // > 运算符
bool operator<=(const BigInteger &b) const { return !(b < *this); } // <= 运算符
bool operator>=(const BigInteger &b) const { return !(*this < b); } // >= 运算符
bool operator!=(const BigInteger &b) const { return b < *this || *this < b; } // != 运算符
bool operator==(const BigInteger &b) const { return !(b < *this) && !(*this < b); } //==运算符
// 逻辑运算符
bool operator||(const BigInteger &b) const { return *this != 0 || b != 0; } // || 运算符
bool operator&&(const BigInteger &b) const { return *this != 0 && b != 0; } // && 运算符
bool operator!() { return (bool)(*this == 0); } // ! 运算符
//重载<<使得可以直接输出大数
friend ostream &operator<<(ostream &out, const BigInteger &x)
{
if (!x.sign)
out << '-';
out << x.num.back();
for (int i = x.num.size() - 2; i >= 0; i--)
{
char buf[10];
//如WIDTH和BASR有变化,此处要修改为%0(WIDTH)d
sprintf(buf, "%08d", x.num[i]);
for (int j = 0; j < strlen(buf); j++)
out << buf[j];
}
return out;
}
//重载>>使得可以直接输入大数
friend istream &operator>>(istream &in, BigInteger &x)
{
string str;
in >> str;
size_t len = str.size();
int start = 0;
if (str[0] == '-')
start = 1;
if (str[start] == '\0')
return in;
for (int i = start; i < len; i++)
{
if (str[i] < '0' || str[i] > '9')
return in;
}
x.sign = !start;
x = str.c_str();
return in;
}
};
int const N = 100 + 10;
BigInteger w[N], f[N][N]; // f[l][r]:l点到r点这些点组成的所以点可以划分的三角形的面积之和
int n;
int main()
{
cin >> n;
BigInteger INF = 1;
INF = INF.e(30);
for (int i = 1; i <= n; i++) cin >> w[i];
for (int len = 3; len <= n; len++) // 枚举区间长度
for (int l = 1; l + len - 1 <= n; l++) // 枚举左端点
{
int r = l + len - 1; // 右端点
f[l][r] = INF; // 一开始为非法状态
for (int k = l + 1; k < r; k++) // 枚举分割点
f[l][r] = min(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]); // dp更新
}
cout << f[1][n] << endl;
return 0;
}
3.3.2 求方案数
2019ICPC沈阳区域赛G-Triangulation
给定n个点,这n个点是顺时针给定或者逆时针给定,n个点组成一个多边形,求把n-多边形划分成三角形的方案数。
样例输入:
6
0 10
2 6
5 3
4 1
10 0
0 0
样例输出:
3
/*
输入的点可能顺时针也可能逆时针,把这些点变换成顺时针(使用叉乘来判断,a和b叉乘大于0表明b在a的逆时针方向)
状态表示:f[l][r]表示利用l~r这些点组成的多边形可以划分成的方案数
状态划分:分割点k
状态转移:f[l][r] = (f[l][r] + f[l][k] * f[k][r]) % mod
入口:f[i][i + 1] = 1;
出口:f[1][n]
转移的时候要注意判断rl向量在lk向量的逆时针方向(保证转移合法,不会出现凹进去构成的三角形)
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 2e3 + 10, MOD = 998244353;
struct POINT
{
LL x, y;
LL operator *(const POINT &b) const
{
return x * b.y - y * b.x;
}
POINT operator -(const POINT &b) const
{
POINT p;
p.x = x - b.x;
p.y = y - b.y;
return p;
}
}p[N];
int f[N][N], n;
int main()
{
cin >> n;
LL s = 0; // 判断顺时针还是逆时针输入
for (int i = 1; i <= n; ++i)
{
cin >> p[i].x >> p[i].y;
s += p[i - 1] * p[i];
}
s += p[n] * p[1];
if (s > 0) // 大于0表示逆时针输入
{
for (int i = 1; i <= n / 2; ++i)
swap(p[i], p[n - i + 1]);
}
for (int i = 1; i < n; ++i) f[i][i + 1] = 1; // 入口
// dp更新
for (int len = 3; len <= n; ++len) // 区间长度
{
for (int l = 1; l + len - 1 <= n; ++l) // 左端点
{
int r = l + len - 1;
for (int k = l + 1; k < r; ++k) // 分割点
if((p[r] - p[l]) * (p[k] - p[l]) > 0) // 合法转移条件
f[l][r] = (f[l][r] + f[l][k] * (LL)f[k][r]) % MOD; // 转移
}
}
cout << f[1][n] << endl;
return 0;
}
3.4 子树划分问题
由于树的遍历答案可以划分为左1子树区间、左2子树区间、…右子树区间、根,因此可以使用区间dp来处理
acwing479加分二叉树
设一个n个节点的二叉树tree的中序遍历为(1,2,3,…,n),其中数字1,2,3,…,n为节点编号。
每个节点都有一个分数(均为正整数),记第i个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
subtree的左子树的加分 × subtree的右子树的加分 + subtree的根的分数
若某个子树为空,规定其加分为1。叶子的加分就是叶节点本身的分数,不考虑它的空子树。
试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。
要求输出:
(1)tree的最高加分
(2)tree的前序遍历
输入格式
第1行:一个整数n,为节点个数。
第2行:n个用空格隔开的整数,为每个节点的分数(0<分数<100)。
输出格式
第1行:一个整数,为最高加分(结果不会超过4,000,000,000)。
第2行:n个用空格隔开的整数,为该树的前序遍历。如果存在多种方案,则输出字典序最小的方案。
数据范围
n<30
输入样例:
5
5 7 1 2 10
输出样例:
145
3 1 2 4 5
/*
f[l][r]表示中序遍历[l, r]的最大值
状态划分使用根节点j
则有状态转移方程:f[l][r] = max(f[l][r], f[l][j - 1] * f[j + 1][r] + w[j]);
进行状态转移即可
为了找出前序遍历,可以维护一个g数组,g[i][j]表示[i, j]序列的跟节点
然后做根左右的dfs即可
为了维护字典序最小的前序遍历,我们只要保证当两个f[l][r]相等时,取j小的那个,那么只需要把分割点j按从小到大枚举即可
*/
#include<bits/stdc++.h>
using namespace std;
int n;
int const N = 50;
int f[N][N], w[N], g[N][N]; // f[l][r]区间[l, r]的最小值,g[l][r] = t 区间[l, r]的根节点为t
// 得到前序遍历:根左右
void dfs(int l, int r)
{
if (l > r) return;
int root = g[l][r];
cout << root << " ";
dfs(l, root - 1);
dfs(root + 1, r);
}
int main()
{
cin >> n;
for (int i = 1; i <= n; ++i) scanf("%d", &w[i]);
for (int len = 1; len <= n; ++len) // 枚举区间长度
{
for (int l = 1; l + len - 1 <= n; ++l) // 左端点
{
int r = l + len - 1; // 右端点
if (len == 1) // 如果区间长度为1,说明是叶节点
{
f[l][r] = w[l];
g[l][r] = l; // 根节点是自己
}
else // 区间长度不为1
{
for (int j = l ; j <= r; ++j) // 枚举中点
{
int left = j == l? 1: f[l][j - 1];
int right = j == r? 1: f[j + 1][r];
int score = left * right + w[j];
if (score > f[l][r]) // 只有score>f[l][r]时才能转移,这样才能得到字典序最小的前序遍历
{
f[l][r] = score;
g[l][r] = j;
}
}
}
}
}
cout << f[1][n] << endl; // 输出最高得分
dfs(1, n); // 输出前序遍历结果
return 0;
}
acwing284金字塔
虽然探索金字塔是极其老套的剧情,但是有一队探险家还是到了某金字塔脚下。
经过多年的研究,科学家对这座金字塔的内部结构已经有所了解。
首先,金字塔由若干房间组成,房间之间连有通道。
如果把房间看作节点,通道看作边的话,整个金字塔呈现一个有根树结构,节点的子树之间有序,金字塔有唯一的一个入口通向树根。
并且,每个房间的墙壁都涂有若干种颜色的一种。
探险队员打算进一步了解金字塔的结构,为此,他们使用了一种特殊设计的机器人。
这种机器人会从入口进入金字塔,之后对金字塔进行深度优先遍历。
机器人每进入一个房间(无论是第一次进入还是返回),都会记录这个房间的颜色。
最后,机器人会从入口退出金字塔。
显然,机器人会访问每个房间至少一次,并且穿越每条通道恰好两次(两个方向各一次), 然后,机器人会得到一个颜色序列。
但是,探险队员发现这个颜色序列并不能唯一确定金字塔的结构。
现在他们想请你帮助他们计算,对于一个给定的颜色序列,有多少种可能的结构会得到这个序列。
因为结果可能会非常大,你只需要输出答案对109 取模之后的值。
输入格式
输入仅一行,包含一个字符串S,长度不超过300,表示机器人得到的颜色序列。
输出格式
输出一个整数表示答案。
输入样例:
ABABABA
输出样例:
5
/*
状态表示:f[l][r]:从l~r这段区间的方案
状态划分:最后一颗子树的起始位置
状态转移:f[l][r] = (f[l][r] + (LL)f[l][k] * f[k + 1][r - 1]) % MOD;
入口:f[l][l] = 1
出口:f[1][n]
dfs序列有几个性质:
1. 长度为2(n-1)+1 = 2n - 1
2. [l, r]中,str[l] == str[r]
本题判断是时候必须满足输入序列长度为奇数
枚举最后一颗子树的起始位置:k=l~r-2,同时要满足str[k] == str[r]才能转移
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 3e2 + 10, MOD = 1e9;
char str[N];
int n, f[N][N];
int main()
{
cin >> str + 1;
n = strlen(str + 1);
if (n % 2 == 0) printf("0\n"); // 长度必须为奇数
else
{
for (int len = 1; len <= n; ++len) // 枚举区间长度
{
for (int l = 1; l + len - 1 <= n; ++l) // 枚举左端点
{
int r = l + len - 1; // 右端点
if (len == 1) f[l][r] = 1; // 入口
else if (str[l] == str[r]) // 必须满足这个条件才可能存在子树
{
for (int k = l; k < r - 1; k += 2) // 枚举最后一颗子树的左端点
{
if (str[k] == str[r]) // 必须满足这个条件才可能存在子树
f[l][r] = (f[l][r] + (LL)f[l][k] * f[k + 1][r - 1]) % MOD; // 转移
}
}
}
}
cout << f[1][n] << endl;
}
return 0;
}
3.5 二维分割问题
区间dp也可以处理对一个二维区间进行分割的情况,因为每个二维区间可以分成从(x1, y1) ~ (x2, y2)的区间,然后可以枚举分割线,变成从(x1, y1)~分割线,分割线~(x2, y2)的两个子状态之和
acwing321棋盘分割
将一个8*8的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了(n-1)次后,连同最后剩下的矩形棋盘共有n块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)
原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。
现在需要把棋盘按上述规则分割成n块矩形棋盘,并使各矩形棋盘总分的均方差最小。
均方差
,其中平均值 ,xi为第 i 块矩形棋盘的总分。
请编程对给出的棋盘及n,求出均方差的最小值。
输入格式
第1行为一个整数n。
第2行至第9行每行为8个小于100的非负整数,表示棋盘上相应格子的分值。每行相邻两数之间用一个空格分隔。
输出格式
输出最小均方差值(四舍五入精确到小数点后三位)。
数据范围
1<n<15
输入样例:
3
1 1 1 1 1 1 1 3
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0
1 1 1 1 1 1 0 3
输出样例:
1.633
/*
本题是二维的区间dp,f[x1][y1][x2][y2][k]:表示{(x1, y1), (x2, y2)}的这个矩形切成k块需要的代价
以最后一刀的位置来划分,最后一刀可以是横的,也可以是竖的,枚举过程可以采用记忆化搜索来优化时间复杂度
如果不用记忆化搜索,那么需要写6个for循环,所以直接记忆化搜索,二维的情况就可以使用记忆化搜索
*/
#include<bits/stdc++.h>
using namespace std;
const int N = 15, M = 9;
const double INF = 1e9;
int n, m = 8;
int s[M][M];
double f[M][M][M][M][N];
double X;
// 求二维前缀和
int get_sum(int x1, int y1, int x2, int y2)
{
return s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
}
// 求计算的公式答案
double get(int x1, int y1, int x2, int y2)
{
double sum = get_sum(x1, y1, x2, y2) - X;
return (double)sum * sum / n;
}
// 当前要分割的区域为(x1, y1)~(x2, y2),还需分割k块
double dp(int x1, int y1, int x2, int y2, int k)
{
double &v = f[x1][y1][x2][y2][k];
if (v >= 0) return v; // 如果枚举过,那么直接返回
if (k == 0) return v = get(x1, y1, x2, y2); // 如果k等于0,表示不需要切割了
v = INF; // 记忆化搜索的结构是一棵树,枚举最小值时初始化为INF
// 枚举横的
for (int i = x1; i < x2; i ++ )
{
v = min(v, get(x1, y1, i, y2) + dp(i + 1, y1, x2, y2, k - 1));
v = min(v, get(i + 1, y1, x2, y2) + dp(x1, y1, i, y2, k - 1));
}
// 枚举竖的
for (int i = y1; i < y2; i ++ )
{
v = min(v, get(x1, y1, x2, i) + dp(x1, i + 1, x2, y2, k - 1));
v = min(v, get(x1, i + 1, x2, y2) + dp(x1, y1, x2, i, k - 1));
}
return v;
}
int main()
{
cin >> n;
// 预处理前缀和
for (int i = 1; i <= m; i ++ )
for (int j = 1; j <= m; j ++ )
{
cin >> s[i][j];
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
}
X = (double)s[m][m] / n;
memset(f, -1, sizeof f);
printf("%.3lf\n", sqrt(dp(1, 1, 8, 8, n - 1)));
return 0;
}