1. Cake
题目链接:
kaungbin传送门
zoj传送门
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1005;
struct Node {
int x, y;
} nodes[N], pack[N];
int cost[N][N], dp[N][N], n, m;
//叉乘公式
int cross(Node A, Node B, Node C) {
return (A.x - C.x) * (B.y - C.y) - (B.x - C.x) * (A.y - C.y);
}
//把所有点按字典序升序排序
bool cmp(Node A, Node B) {
if (A.y == B.y) return A.x < B.x;
return A.y < B.y;
}
//Graham扫描法
int Graham() {
//先排序
sort(nodes, nodes + n, cmp);
//第一个点一定是凸包上的点
pack[0] = nodes[0];
//先放进一个点
pack[1] = nodes[1];
//当前凸包顶点数
int top = 1;
//构造凸包的下侧
for (int i = 0; i < n; i++) {
while (top && cross(pack[top], nodes[i], pack[top - 1]) >= 0) top--;
pack[++top] = nodes[i];
}
//构造凸包上侧
for(int i = n - 2, k = top; i >= 0; i--) {
while (top > mid && cross(pack[top], nodes[i], pack[top - 1]) >= 0) top--;
pack[++top] = nodes[i];
}
//返回凸包上有多少点
return top;
}
//计算划分三角的权值
int cal(Node A, Node B) {
return (abs(A.x + B.x) * abs(A.y + B.y)) % m;
}
int main() {
while (~scanf("%d%d", &n, &m)) {
for (int i = 0; i < n; i++)
scanf("%d%d",&nodes[i].x, &nodes[i].y);
//判断是否为凸包
if (Graham() != n) printf("I can't cut.\n");
else {
//计算不同点构造三角形所需的权值
memset(cost, 0, sizeof(cost));
for (int i = 0; i < n; ++i)
for (int j = i + 2; j < n; ++j)
cost[i][j] = cost[j][i] = cal(pack[i], pack[j]);
//区间dp部分
//枚举区间长度
for(int len = 3; len <= n; len++) {
//枚举起点
for(int i = 0; i < n - len + 1; i++) {
//处使化值
int l = i, r = i + len - 1;
dp[l][r] = 0x3f3f3f3f;
//枚举分隔点
for(int j = l + 1; j < r; j++) {
dp[l][r] = min(dp[l][r], dp[l][j] + dp[j][r] + cost[l][j] + cost[j][r]);
}
}
}
//输出答案
printf("%d\n", dp[0][n - 1]);
}
}
return 0;
}
这道题是两种题型的结合,一个是通过凸包求多边形是否为凸多边形,二是通过区间dp划分三角形
凸包方面:
本题要我们判断当前多边形是否为凸多边形,判断方法就是先求出该多边形的凸包中包含的点的个数,如果点的个数和该多边形的点的个数一样多,那么该多边形就是一个凸多边形。反之,则是一个凹多边形,输出 “I can’t cut.” 即可。
区间dp方面:
按照常规思路枚举区间长度,枚举起点,枚举分隔点。
注意区间长度要从三开始,因为三角形一定要有三个顶点。
在一开始区间的dp值要初始化为正无穷(取一个很大的值即可)
转移方程为dp[l][r] = min(dp[l][r], dp[l][j] + dp[j][r] + cost[l][j] + cost[j][r]) 我们每次从中一个多变形中找到一个分隔点,那么三角形划分时就加入了边 lj ,rj 产生的价值,我们枚举所有分割点找到消耗最小的方案即可。
2. Halloween Costumes
题目链接:
传送门
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1010;
int t, ca, n, a[N], dp[N][N];
int main() {
scanf("%d", &t);
while(t--) {
scanf("%d", &n);
//初始化
memset(dp, 0, sizeof dp);
memset(a, 0, sizeof a);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
dp[i][i] = 1;
}
//枚举区间长度
for(int len = 2; len <= n; len++) {
//枚举起点
for(int i = 1; i <= n - len + 1; i++) {
//处使化起点和终点
int l = i, r = i + len - 1;
dp[l][r] = dp[l][r - 1] + 1;
//枚举分割点
for(int k = l; k < r; k++) {
if(a[k] == a[r]) {
//通过转移方程得到最优解
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r - 1]);
}
}
}
}
//输出结果
printf("Case %d: %d\n", ++ca, dp[1][n]);
}
}
这道题可以用区间dp的方法来做。
首先讲讲区间dp的大致思路:
枚举区间长度
枚举区间起点
处使化dp值
枚举分隔点
转移方程得到不同合并点下的最优解
输出结果
----------------------------------------------------------------------------------------
接下来结合题来谈谈这道题怎么做
首先区间表示的就是某段区域内的最优解
l 代表左边界
r 代表右边界
初始化是我们可以处使化为dp[l][r] = dp[l][r - 1] + 1每次最差的情况便是在左边的区间中加一,至于可否可以通过不穿(即脱掉外层衣服)的方法就留给后面dp讨论了。
接下来讲讲这道题的转移方程,如果我们要像保留某件衣服留到后面某个位置通过脱的方式得到相应衣服,那么我们可以假设当前衣服保留,那么区间的后半段我们可以视为从头开始算。因此转移方程为:dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r - 1])。
最后注意dp数组的处使化(单点处使化为1)就可以了。
3. Brackets
题目链接:
传送门
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int dp[N][N];
string str;
int main() {
while(cin >> str, str != "end") {
memset(dp, 0, sizeof dp);
int n = str.size();
//枚举区间长度
for(int len = 2; len <= n; len++) {
//枚举起点
for(int i = 0; i < n - len + 1; i++) {
int l = i, r = i + len - 1;
//初始化dp[l][r]的值
if((str[l] == '(' && str[r] == ')') || (str[l] == '[' && str[r] == ']')) dp[l][r] = dp[l + 1][r - 1] + 2;
//枚举分割点
for(int k = l; k < r; k++) {
dp[l][r] = max(dp[l][r], dp[l][k] + dp[k + 1][r]);
}
}
}
cout << dp[0][n - 1] << endl;
}
}
这是一道区间dp的题
首先根据区间dp的套路,我们直接可以枚举长度,枚举起点,枚举分割点
接下来我们要讨论初始化以及转移方程的问题:
首先初始化方面,首先,讨论第二种情况(结合上条件一)。我们知道,如果括号匹配,那当前dp的初始化长度就等于在此之前,两端点间(不含端点)的最长字段长度。即dp[l][r] = dp[l + 1][r - 1] + 2;
接着讲讲转移方程,转移方程我们就要结合条件三来考虑。条件三我们可以看出就是合并的操作,那么我们只要枚举区间内分割点,找到分割点两边区间合并的最大值即可。
4. Coloring Brackets
题目链接:
传送门
#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<algorithm>
#include<stack>
#define ll long long
using namespace std;
const int N = 710;
const int M = 1e9 + 7;
ll dp[N][N][3][3], match[N];
stack<int> stk;
char bracket[N];
//记录下一对括号中,两个括号的相应下标
void getMatch() {
scanf("%s", bracket + 1);
int n = strlen(bracket + 1);
for(int i = 1; i <= n; i++) {
if(bracket[i] == '(') stk.push(i);
else {
match[i] = stk.top();
match[stk.top()] = i;
stk.pop();
}
}
}
void dfs(int l, int r) {
//匹配到一对内层无括号的括号
if(l + 1 == r) {
//初始化各值
dp[l][r][1][0] = 1;
dp[l][r][2][0] = 1;
dp[l][r][0][1] = 1;
dp[l][r][0][2] = 1;
return;
}
//匹配到一对内层有括号的括号
if(match[l] == r) {
//搜索里面的情况
dfs(l + 1, r - 1);
//枚举不同颜色的情况
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++) {
//相邻括号同色的情况不合法,所以相邻括号同色的方案不能加入
if(i != 1) dp[l][r][1][0] = (dp[l][r][1][0] + dp[l + 1][r - 1][i][j]) % M;
if(i != 2) dp[l][r][2][0] = (dp[l][r][2][0] + dp[l + 1][r - 1][i][j]) % M;
if(j != 1) dp[l][r][0][1] = (dp[l][r][0][1] + dp[l + 1][r - 1][i][j]) % M;
if(j != 2) dp[l][r][0][2] = (dp[l][r][0][2] + dp[l + 1][r - 1][i][j]) % M;
}
}
//区间左右端点不是一堆匹配括号的情况
} else {
//找到与左端点匹配的右括号
int k = match[l];
//分别找两个区间
dfs(l, k);
dfs(k + 1, r);
//枚举所有涂色情况
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++) {
for(int p = 0; p < 3; p++) {
for(int q = 0; q < 3; q++) {
//两区间中间的位置两个点不能涂色(都不涂色除外)
if(p != q || q == 0) dp[l][r][i][j] = (dp[l][r][i][j] + dp[l][k][i][p] * dp[k + 1][r][q][j]) % M;
}
}
}
}
}
}
int main() {
getMatch();
int n = strlen(bracket + 1);
dfs(1, n);
ll ans = 0;
//把不同涂色情况的答案加起来
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++) {
ans = (ans + dp[1][n][i][j]) % M;
}
}
printf("%lld\n", ans);
}
这道题可以用区间dp来做
首先我们要对这一堆括号进行匹配找出他们对应括号的位置,这一步主要是为了讨论每对匹配的括号,有且仅有一个字符被染色的情况做铺垫的。
接下来我们来讨论一下三个条件有什么用:
1.每个字符有三种情况:不染色,染成红色,染成蓝色
针对这个条件,我们可以创建一个四维的dp数组,第一二维表示区间的左右端点的坐标,第三维用于记录左端点的涂色情况,而第四维我们可以记录右端点的涂色情况。
2.每对匹配的括号,有且仅有一个字符被染色
由于每对括号仅有一个字符被染色因此可以分四种情况讨论:
1)左端点涂红色,右端点不涂色
2)左端点涂蓝色,右端点不涂色
3)左端点不涂色,右端点涂红色
4)左端点不涂色,右端点涂蓝色
同时针对一对匹配的括号又有两种情况
a. 匹配的括号内没有括号了,那么我们对这对括号内四种情况分别初始化为1即可。
b. 匹配的括号内还有括号,那么我们就要讨论里面括号的情况,讨论时我们还需要结合外层括号讨论(具体见条件3)
3.所有相邻的两个字符,不能染成同一种颜色
因为这个条件我们在讨论内层括号时只能算相邻括号不相同的情况(即当前左括号与其右边一个括号不能同色,当前右括号与其左边一个括号不能同色)
----------------------------------------------------------------------------------------------------------------------------------------------
针对上面讲的内容,我们考虑一下如何分区dfs讨论情况。
- 搜到一对匹配的括号且内层无括号的情况:
那此时初始化即可- 搜到一对匹配的括号且内层有括号的情况:
那此时我们讨论内层的情况即可(同时要注意相邻括号不同色的情况)- 搜到一对不匹配的括号:
那此时我们把区间划分成两份:左端点的左括号到与左端点匹配的右括号的一段区间,和剩下的区间,由于区间与区间是独立的所以此时答案的方案数由两区间方案数相乘得到,但需要注意两端区间中间的分隔位置的两个点不能同色
5. Multiplication Puzzle
题目链接:
传送门
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const int N = 110;
ll n;
ll a[N];
ll dp[N][N];
int main() {
scanf("%lld", &n);
//初始化
memset(dp, 0x3f, sizeof dp);
for(ll i = 1 ; i <= n; i++) scanf("%lld", &a[i]), dp[i][i] = 0, dp[i][i + 1] = dp[i - 1][i] = 0;
//枚举长度(3以下不存在合并)
for(ll len = 3; len <= n; len++) {
//枚举起点
for(ll l = 1; l + len - 1 <= n; l++) {
//计算出右顶点
ll r = l + len - 1;
//枚举分割点
for(ll k = l + 1; k < r; k++) {
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k][r] + a[l] * a[k] * a[r]);
}
}
}
printf("%lld\n", dp[1][n]);
}
这道题算是比较传统的区间dp题,特别需要注意的是初始化的问题以及区间合并的代价计算。首先由于要计算最小值,所以所有区间我们一开始都初始化为最大值,但有些边界我们要初始化为0,比如只有一个元素以及只有两个连续的情况(因为两个以下不存在合并)。接着我们从最简单的小区间推起a, b, c三个元素时那么dp[l][r] = a * b * c,更一般化以后我们就会推出状态转移方程为dp[l][r] = min(dp[l][r], dp[l][k] + dp[k][r] + a[l] * a[k] * a[r])。最后输出答案即可。
6. Food Delivery
题目链接:
kaungbin传送门
zoj传送门
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010;
struct Node{
int idx, v;
bool operator < (const Node &t) const {
return idx < t.idx;
}
}a[N];
int n, v, x;
//0表示在当前点左边,1表示在当前点右边
int sum[N], dp[N][N][2];
//计算除了某个区间以外所有点的不满意值
int cal(int l, int r) {
return sum[n] - sum[r] + sum[l - 1];
}
int main() {
while(~scanf("%d%d%d", &n, &v, &x)) {
for(int i = 1; i <= n; i++) scanf("%d%d", &a[i].idx, &a[i].v);
sort(a + 1, a + n + 1);
memset(dp, 0x3f, sizeof dp);
//维护前缀和,方便后面计算
for(int i = 1; i <= n; i++) sum[i] = sum[i - 1] + a[i].v;
//初始化一开始走到某点的值
for(int i = 1; i <= n; i++) dp[i][i][0] = dp[i][i][1] = abs(a[i].idx - x) * sum[n];
//枚举长度
for(int len = 2; len <= n; len++) {
//枚举左端点
for(int l = 1; l + len - 1 <= n; l++) {
//枚举右端点
int r = l + len - 1;
//维护两种状态下的dp值
dp[l][r][0] = min(dp[l + 1][r][0] + abs(a[l + 1].idx - a[l].idx) * cal(l + 1, r), dp[l + 1][r][1] + abs(a[r].idx - a[l].idx) * cal(l + 1, r));
dp[l][r][1] = min(dp[l][r - 1][0] + abs(a[r].idx - a[l].idx) * cal(l, r - 1), dp[l][r - 1][1] + abs(a[r].idx - a[r - 1].idx) * cal(l, r - 1));
}
}
printf("%d\n", min(dp[1][n][0], dp[1][n][1]) * v);
}
}
这道题除了传统的记录区间值以外还需要维护其最后点的信息,其实这道题与洛谷关路灯那一题非常相似,是这道题的简单版,如果没思路的话建议做一下那道题再来做这道题。
接下来讲讲这道题,这道题dp不能只维护左右范围了,还需要维护当前点走到了这个区间的左端点和右端点,每次我们走的时候都有两种走法,往左走,往右走,每次我们就一次维护区间最值即可。还有值得注意的就是初始化的问题,我们可以假设我们一开始先走到某个点,那么这个点就是dp[i][i][0/1]的值
7. You Are the One
题目链接:
传送门
//这道题子问题的分割上感觉还是有点难度的qwq
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int t, n, ca;
int a[N], sum[N];
int dp[N][N];
int main() {
scanf("%d", &t);
while(t--) {
scanf("%d", &n);
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]), sum[i] = sum[i - 1] + a[i];
//枚举长度
for(int len = 2; len <= n; len++) {
//枚举左端点
for(int l = 1; l + len - 1 <= n; l++) {
//计算出右端点
int r = l + len - 1;
//第一个第一个开始
dp[l][r] = dp[l + 1][r] + sum[r] - sum[l];
for(int k = l + 1; k <= r; k++) {
dp[l][r] = min(dp[l][r], a[l] * (k - l) + dp[l + 1][k] + dp[k + 1][r] + (k - l + 1) * (sum[r] - sum[k]));
}
}
}
printf("Case #%d: %d\n", ++ca, dp[1][n]);
}
}
这道题感觉最大的难点在于状态转移时区间合并的方法
从头分析一波这道题,首先我们从最底层(两个元素)开始分析起来
当只有两个元素时显然只有两种情况第一个元素先,或者第二个元素先
当有n个元素时,我们应该如何枚举分割点呢?
其实可以选取当前区间的第一个点的出场时间作为分割点
然后我们先独立计算出第一点的等待时间,然后把前半部分的dp值与后半部分相加,并加上后半区间的等待时间
最终就可以得到答案了
8. String painter
题目链接:
传送门
//洛谷涂色那一题的拓展版
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int n, m;
int dp[N][N], ans[N];
char s[N], t[N];
int main() {
while(~scanf("%s%s", s + 1, t + 1)) {
memset(dp, 0x3f, sizeof dp);
n = strlen(s + 1);
//这一部分是对涂色的预处理,即假如这个区间需要由涂色处理获得,那么需要涂多少此颜色(我们假设每个字母是一种颜色)
for(int i = 1;i <= n; i++) dp[i][i] = 1;
//枚举区间长度
for(int len = 2; len <= n; len++) {
//枚举左端点
for(int l = 1; l + len - 1 <= n; l++) {
//计算右端点
int r = l + len - 1;
//左右端点颜色相等的情况
if(t[l] == t[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]);
}
}
//第二次dp寻找最优解
for(int i = 1; i <= n; i++) {
//首先假设前i个字符都是涂出来的
ans[i] = dp[1][i];
//如果第i个字母都相同那么我们取前i - 1个的最优解即可
if(s[i] == t[i]) ans[i] = ans[i - 1];
else {
//反之我们需要寻找一下从第i个字符开始往前涂多少个能得到最优解
for(int k = 1; k < i; k++) ans[i] = min(ans[i], ans[k] + dp[k + 1][i]);;
}
}
printf("%d\n", ans[n]);
}
}
做这道题之前非常推荐先去完成洛谷涂色那道题,这道题的预处理与那道题可以说是一样的
这道题需要做两次dp
第一次我们先做第一次dp预处理出某段区间中的如果通过涂色得到需要涂多少次
第二次dp我们去求每次我们需要涂那些区间
详情见代码