文章目录
- 零碎知识点
- 题目
- 1025 - A Spy in the Metro
- 437 - The Tower of Babylon
- 1347 - Tour
- 116 - Unidirectional TSP
- 12563 - Jin Ge Jin Qu hao
- 11400 - Lighting System Design
- 1625 - Color Length
- 11584 - Partitioning by Palindromes
- 10003 - Cutting Sticks
- 1626 - 括号序列
- 10285 - Longest Run on a Snowboard
- 10118 - Free Candies
- 1629 - Cake slicing
零碎知识点
递推法和记忆化搜索法的思考
- 二者均可以接动态规划题目,均后动态规划思想,记忆化搜索使用递归,但是将值存储起来避免了重复计算,但是依然耗费递归时间。
- 记忆化搜索一般函数为solve(i, j)即将dp[i][j]所以依赖的值计算出来,不能将所有值计算出来,因此徐亚在其外层再套一层循环,所以效率比递推法低。
- 记忆化搜索不需要考虑dp数组更新的顺序,之间按照状态转移方程递归就行,只不过需要先判断值是否已经计算,并且dp数组初始化一般为-1。而递归法虽然效率高,但是需要考虑更新顺序。
题目
1025 - A Spy in the Metro
题目链接:1025 - A Spy in the Metro
参考博文:UVA_1025_A_Spy_in_the_Metro_(动态规划)
- 题目大意:某城市的地铁是线性的,有n个车站,有M1辆列车从左到右开,M2辆列车从右到左开.在0时刻,你在第一站,要在T时刻到达第n站,其间可以随意换车,要求在车站等待的时间最短。
- 思路:
- 状态构造:dp[i][j]表示时刻i,在车站j最少还需要等待的时间。
- 目标状态:dp[0][1]
- 状态转移方程:有三个策略。
dp[i][j]=1.dp[i+1][j]+1(在车站等一分钟).
2.dp[i+t[j]][j+1] (如果有,乘坐向右的列车).
3.dp[i+t[j-1]][j-1] (如果有,乘坐向左的列车). - 初始化(边界条件):dp[T][n] = 0,dp[T][j]=INF(j!=n)
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int INF = 1<<29;
int dp[500][100], t[500];
int has_train[500][100][3];
int n, M1, m1, M2, m2, T;
void Init()
{
memset(has_train, 0, sizeof(has_train));
}
int main()
{
int kase = 1;
while(scanf("%d", &n)!=EOF && n)
{
Init();
scanf("%d", &T);
for(int i=1; i<n; i++)
scanf("%d", &t[i]);
scanf("%d", &M1);
//初始化
for(int i=0; i<M1; i++)
{
scanf("%d", &m1);
for(int j=1; j<=n; j++)
{
has_train[m1][j][0] = 1;
m1 += t[j];
}
}
scanf("%d", &M2);
for(int i=0; i<M2; i++)
{
scanf("%d", &m2);
for(int j=n; j>=1; j--)
{
has_train[m2][j][1] = 1;
m2 += t[j-1];
}
}
for(int i=1; i<n; i++) dp[T][i] = INF;
dp[T][n] = 0;
for(int i=T-1; i>=0; i--)
{
for(int j=1; j<=n; j++)
{
dp[i][j] = dp[i+1][j] + 1;//在此地等待一分钟
if(j<n && has_train[i][j][0] && i+t[j]<=T)
dp[i][j] = min(dp[i][j], dp[i+t[j]][j+1]);//直接向右坐地铁
if(j>1 && has_train[i][j][1] && i+t[j-1]<=T)
dp[i][j] = min(dp[i][j], dp[i+t[j-1]][j-1]);//直接向左坐地铁
}
}
printf("Case Number %d: ", kase++);
if(dp[0][1]>=INF) printf("impossible\n");
else printf("%d\n", dp[0][1]);
}
return 0;
}
437 - The Tower of Babylon
题目链接:437 - The Tower of Babylon
- 题目大意:给n中立方体,每种无限多个,求能堆成塔的最高高度(必须严格满足上面的长宽小于下面的)。
- 思路:该题目可以转换为DAG上的最长路。
- 我的想法:
- 状态构造:dp[idx, k]表示以编号为idx,高为第k维状态为最底层所能获取的最大高度。
- 终态:max{dp[,]}
- 状态转移方程:dp[idx, k] = max{d[i, j]}+a[idx][k]
- 初始化:dp[,]=0
- 我的想法:
我的AC代码,有点冗长,性能不好
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n;
int dp[50][4], a[50][4];
void Init(int n)
{
for(int i=0; i<=n; i++)
{
for(int j=0; j<3; j++)
dp[i][j] = 0;
}
}
//检查a[k][t]是否可以在a[i][j]上
bool check(int i, int j, int k, int t)
{
int m1[3], m2[3], cnt1 = 0, cnt2 = 0;
for(int p=0; p<3; p++)
{
if(p!=j)
m1[cnt1++] = a[i][p];
if(p!=t)
m2[cnt2++] = a[k][p];
}
for(int p=0; p<2; p++)
{
if(m1[p]<=m2[p]) return 0;
}
return 1;
}
//DAG记忆化搜索
int solve(int i, int j)
{
int &ans = dp[i][j];
if(ans>0) return ans;
ans = a[i][j];
int top = 0;
for(int k=0; k<n; k++)
{
for(int t=0; t<3; t++)
{
if(check(i, j, k, t))
top = max(top, solve(k, t));
}
}
ans = top+ans;
return ans;
}
int main()
{
int kase = 1;
while(scanf("%d", &n)!=EOF && n)
{
for(int i=0; i<n; i++)
{
for(int j=0; j<3; j++)
{
scanf("%d", &a[i][j]);
}
sort(a[i], a[i]+3);
}
Init(n);
int Max = 0;
//任何一个状态均可以作为起始点
//寻找路径最长的状态
for(int i=0; i<n; i++)
{
for(int j=0; j<3; j++)
Max = max(Max, solve(i, j));
}
printf("Case %d: maximum height = %d\n", kase++, Max);
}
return 0;
}
网上代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=30+2;
struct node{
int x,y,z;
node(int x=0,int y=0,int z=0):x(x),y(y),z(z){}
bool operator<(const node& n)const {//用于判断二者是否可以相连
return x<n.x&&y<n.y || x<n.y&&y<n.x;
}
}nt[maxn*3];
int g[maxn*3][maxn*3];
int d[maxn*3];//表示以第i个状态作为底的高度
int n;
int dp(int i,int h){
int& ans=d[i];
if(ans>0)return ans;
ans=h;
for(int j=0;j<n*3;j++)
if(g[i][j])ans=max(ans,dp(j,nt[j].z)+h);//当第j个状态可以放在第i个状态上方时
return ans;
}
int main(){
int count1=0;
while(scanf("%d",&n)==1 && n){
int c=0;
for(int i=0;i<n;i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
nt[c++]=node(x,y,z); //第三位表示高度
nt[c++]=node(x,z,y);
nt[c++]=node(y,z,x);
}
memset(d,0,sizeof(d));
memset(g,0,sizeof(g));
for(int i=0;i<n*3;i++)
for(int j=0;j<n*3;j++)
if(nt[i]<nt[j])g[j][i]=1;//表示第i个状态可以放在第j个状态上方
int ans=-1e5;
for(int i=0;i<n*3;i++)
ans=max(ans,dp(i,nt[i].z));
printf("Case %d: maximum height = %d\n",++count1,ans);
}
return 0;
}
1347 - Tour
题目链接:1347 - Tour
参考博文:UVA 1347 Tour
- 题目大意:给定平面上n(n<=1000)个点的坐标(按照x递增的顺序给出。各点x坐标不同,且均为整数),你的任务是设计一条路线,从最左边的点出发走到最右边的点再返回,要求除了最左边和最右边之外,每个点恰好经过一次,且路径总长度最短,两点间的长度为它们的欧几里得距离。
- 思路:将来回的路程分成两个人同时从同一个起始点出发。
- 状态构造:dp[i][j]表示两个人分别到达第i个和第j个点离目标点距离之和。
- 终态:dp[1][1]
- 状态转移方程:dp[i][j] = min(dist[i][i+1]+dp[i+1, j], dist[j][i+1]+dp[i+1, i]);dist[i][j]表示第i个点和第j个点的距离。
- 初始化:dp(n-1,j)=dist(n-1,n)+dist(j,n)
- 具体解释见参考博文。
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
const int MAX = 1001;
int n;
struct Node
{
int x, y;
};
Node point[MAX];
double dist[MAX][MAX];
double dp[MAX][MAX];
double DP(int i, int j)
{
double &ans = dp[i][j];
if(ans>0) return ans;//表明该值已经计算出
ans = min(dist[i][i+1]+DP(i+1, j), dist[j][i+1]+DP(i+1, i));
return ans;
}
double compute(int i, int j)
{
return sqrt(1.0*(point[i].x - point[j].x)*(point[i].x - point[j].x) + 1.0*(point[i].y - point[j].y)*(point[i].y - point[j].y));
}
int main()
{
while(scanf("%d", &n)!=EOF && n)
{
for(int i=1; i<=n; i++)
scanf("%d%d", &point[i].x, &point[i].y);
//初始化
for(int i=1; i<=n; i++)
{
for(int j=1; j<=n; j++)
{
dp[i][j] = -1.0;
dist[i][j] = dist[j][i] = compute(i, j);
}
}
for(int i=1; i<=n; i++)
dp[n-1][i] = dist[n-1][n]+dist[i][n];
printf("%.2f\n", DP(1, 1));
}
return 0;
}
116 - Unidirectional TSP
- 题目大意:求从第一列到最后一列的一个字典序最小的最短路,要求不仅输出最短路长度,还要输出字典序最小的路径。
- 思路:这道题的状态构造及其转移方程比较好构造,主要是路径不好保存。
- 状态构造:dp[i][j]表示在坐标(i, j)处还需要多少代价才能到达最后一列。
- 目标态:min(dp[*][1])
- 状态转移方程:dp[i][j] = min(dp[i+k][j+1]+c[i][j])(k=-1, 0, 1),其中c[i][j]表示(i, j)处的值。
- 初始化(边界条件):dp[][n] = c[][n]。
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int INF = 1<<29;
int G[15][105], dp[15][105];
int m, n;
int main()
{
int next[15][105];
while(scanf("%d%d", &m, &n)!=EOF)
{
//输入
for(int i=0; i<m; i++)
{
for(int j=0; j<n; j++)
{
scanf("%d", &G[i][j]);
}
}
int ans = INF, first = 0;//ans是最短路径值,first是最短路的起始行
for(int j=n-1; j>=0; j--)
{
for(int i=0; i<m; i++)
{
if(j==n-1) dp[i][j] = G[i][j];//边界条件
else
{
int rows[3] = {i, i-1, i+1};
if(i==0) rows[1] = m-1;
if(i==m-1) rows[2] = 0;
sort(rows, rows+3);//按序,方便找到字典序最小
dp[i][j] = INF;
for(int k=0; k<3; k++)
{
int v = dp[rows[k]][j+1]+G[i][j];
if(v<dp[i][j])
{
dp[i][j] = v;
next[i][j] = rows[k];//(i,j)点的点的行
}
}
}
if(j==0 && dp[i][j]<ans)//找到最短路的起始点
ans = dp[i][j], first = i;
}
}
printf("%d", first+1);
for(int i=next[first][0], j=1; j<n; i=next[i][j], j++)
printf(" %d", i+1);
printf("\n%d\n", ans);
}
return 0;
}
12563 - Jin Ge Jin Qu hao
题目链接:12563 - Jin Ge Jin Qu hao
- 题目大意:假设你正在唱KTV,还剩t秒时间。你决定接下来只唱你最爱的n首歌(不含《劲歌金曲》)中的一些,在时间结束之前再唱一个《劲歌金曲》,使得唱的总曲目尽量多(包含《劲歌金曲》),在此前提下尽量晚的离开KTV。
输入n(n<=50),t(t<=10的9次方)和每首歌的长度(保证不超过3分钟),输出唱的总曲目以及时间总长度。输入保证所有n+1首曲子的总长度严格大于t。 - 思路:需要唱的歌最多,且时间最长,则需要建立两个状态。
- 状态构造:dp[i][j]表示在前i个歌中选择歌使得唱歌时间小于j的最长时间。num[i][j]表示在前i个歌中选择歌使得唱歌时间小于j的最多数目。
- 目标态:num[n][t],dp[n][t]
- 状态转移方程:
当num[i-1][j-t[i]]+1 > num[i][j] || (num[i-1][j-t[i]]+1 == num[i][j] && dp[i-1][j-t[i]]+t[i] > dp[i][j])(表示当选择该歌歌的数目增加||(歌的数目不增加&&增加该歌歌的时长增加)时,num[i][j] = num[i-1][j-t[i]]+1,dp[i][j] = dp[i-1][j-t[i]]+t[i]。 - 初始化:dp[][] = 0, num[][] = 0
代码:
#include <bits/stdc++.h>
using namespace std;
const int MAX = 10000;
int n;
int m;
int num[55][MAX], dp[55][MAX], t[55];
int main()
{
int T, kase = 1;
scanf("%d", &T);
while(T--)
{
memset(num, 0, sizeof(num));
memset(dp, 0, sizeof(dp));
scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++)
scanf("%d", &t[i]);
for(int i=1; i<=n; i++)
{
for(int j=0; j<=m; j++)
{
if(j>t[i])//注意不能带等于号,因为结束时不能开始新歌
{
dp[i][j] = dp[i-1][j]; num[i][j] = num[i-1][j];
//表示当选择该歌歌的数目增加||(歌的数目不增加&&增加该歌歌的时长增加)
if(num[i-1][j-t[i]]+1 > num[i][j] || (num[i-1][j-t[i]]+1 == num[i][j] && dp[i-1][j-t[i]]+t[i] > dp[i][j]))
{
num[i][j] = num[i-1][j-t[i]]+1;
dp[i][j] = dp[i-1][j-t[i]]+t[i];
}
}
else//
{
num[i][j] = num[i-1][j];
dp[i][j] = dp[i-1][j];
}
}
}
printf("Case %d: %d %d\n", kase++, num[n][m]+1, dp[n][m]+678);
}
return 0;
}
11400 - Lighting System Design
题目链接:11400 - Lighting System Design
- 题目大意:给出n个模式,每个模式有电压v,电压费用k,每盏灯的花费c以及灯数l。然后电压高的可以用于电压低的。问说最少花费多少钱可以满足n个模式。
- 思路:每种电压的灯泡要么全换,要么都不换,不然两种电源都不要。因为低电压灯泡可以用较高的电源。按电压从低到高排一遍。设s[i] 前 i 种灯泡的总数量, d[i] 为灯泡1~i的最小开销,d[i] = min(d[j]+(s[i]-s[j])*c[i]+k[i]),前 j 个先用最优方案,后面的 j+1~i都用第 I 号的电源。
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int INF = 1<<29;
const int MAX = 1005;
struct Node
{
int v, l, k, c;
bool operator < (const Node &A) const
{
return v<A.v;
}
};
Node A[MAX];
int s[MAX], dp[MAX];
int main()
{
#ifdef ONLINE_JUDGE
#else
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
#endif
int n;
while(scanf("%d", &n)!=EOF && n)
{
for(int i=1; i<=n; i++)
{
scanf("%d%d%d%d", &A[i].v, &A[i].k, &A[i].c, &A[i].l);
}
sort(A+1, A+n+1);
s[0] = 0;//注意s数组一定在排序后再赋值
for(int i=1; i<=n; i++)
s[i] = s[i-1]+A[i].l;
fill(dp, dp+n+1, INF);
dp[0] = 0;
for(int i=1; i<=n; i++)
{
//两种状态转化的书写方式
for(int j=i-1; j>=0; j--)
{
dp[i] = min(dp[i], dp[j]+(s[i]-s[j])*A[i].c+A[i].k);
}
/*
dp[i] = s[i]*A[i].c+A[i].k;
for(int j=1; j<=i; j++)
{
dp[i] = min(dp[i], dp[j]+(s[i]-s[j])*A[i].c+A[i].k);
}*/
}
printf("%d\n", dp[n]);
}
return 0;
}
1625 - Color Length
题目链接:1625 - Color Length
参考博文:Color Length(UVA-1625)(DP LCS变形)
博文
- 题目大意:输入两个长度分别为n,m(<5000)的颜色序列。要求按顺序合成同一个序列,即每次可以把一个序列开头的颜色放到新序列的尾部。
然后产生的新序列中,对于每一个颜色c,都有出现的位置,L©表示最小位置和最大位置之差,求L©总和最小的新序列。 - 思路:
- 状态构造:dp[i][j]表示在第一个序列加入i个,第二个序列加入j个后,最小L©。c[i][j]表示在第一个序列加入i个,第二个序列加入j个后,已经开始还没有结束的颜色的个数。
- 终态:dp[n][m]
- 状态转移方程:dp[i][j] = min(dp[i-1][j]+c[i-1][j],dp[i][j-1]+c[i][j-1]),表示选择第一个序列或选择第二个序列。
- 边界条件:dp[][] = 0
可以将二维数组转换为一维数组。
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 5000+5;
const int INF = 1000000000;
char p[maxn],q[maxn];
int sp[26],sq[26],ep[26],eq[26];
int d[maxn],c[maxn];//其他人博客貌似都是两层,但其实一层就够了。
//d[]表示在第二个串放入j个时,位置差之和,c[]表示在第二个串放入j个时,已经开始但未结束的字符的个数
int main()
{
int T;
cin>>T;
while(T--)
{
scanf("%s%s",p+1,q+1);//下标从1开始
int n = strlen(p+1),m = strlen(q+1);
for(int i=1;i<=n;i++) p[i]-='A';
for(int j=1;j<=m;j++) q[j]-='A';
for(int i=0;i<26;i++)
{
sp[i] = sq[i] = INF;
ep[i] = eq[i] = 0;
}
//get到这个预处理,以前还没遇到过,标记每一个字符的最开始下标和最末尾下标
for(int i=1;i<=n;i++)
{
sp[p[i]] = min(sp[p[i]],i);
ep[p[i]] = i;
}
for(int i=1;i<=m;i++)
{
sq[q[i]] = min(sq[q[i]],i);
eq[q[i]] = i;
}
memset(c,0,sizeof c);
memset(d,0,sizeof d);
for(int i=0;i<=n;i++)
{
for(int j = 0;j<=m;j++)//表示在第一个串已经放入i个时,开始放第二个串
{
if(!j&&!i) continue;//两个都是0,就直接继续。
int v1=INF,v2 = INF;//对于这种特殊情况,只能先把两个值先寄存在v1,v2,并且赋值为INF。
if(i) v1=d[j]+c[j];
if(j) v2=d[j-1]+c[j-1];
d[j] = min(v1,v2);
if(i)//当i不等于0,判断是否有新字符开始或旧字符结束
{
c[j] = c[j];
if(sp[p[i]]==i&&sq[p[i]]>j)c[j]++;//当该字符已经放入,
if(ep[p[i]]==i&&eq[p[i]]<=j)c[j]--;
}
else if(j)//当i等于0,判断是否有新字符开始或旧字符结束
{
c[j] = c[j-1];
if(sq[q[j]] == j && sp[q[j]]>i)c[j]++;
if(eq[q[j]] == j && ep[q[j]]<=i)c[j]--;
}
}
}
printf("%d\n",d[m]);
}
return 0;
}
11584 - Partitioning by Palindromes
题目链接:11584 - Partitioning by Palindromes
- 题目大意:求字符串中回文子串的最少个数。
- 思路:dp[i]的含义是前i个字符组成的字符串所能划分成的最少回文串的个数。dp[i] = min(dp[i], dp[j] + 1)其中j+1到i的字符串时回文串。
代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000 + 10;
const int INF = 0x3f3f3f3f;
char str[maxn];
int is_palindromes[maxn][maxn];
int dp[maxn];
//递归判断是否是回文串,值得学习
int Is_palindromes(int j, int i) {
if (j >= i) return 1;
if (is_palindromes[j][i] != -1) return is_palindromes[j][i];
if (str[i] == str[j]) {
return is_palindromes[j][i] = Is_palindromes(j + 1, i - 1);
}
else return is_palindromes[j][i] = 0;
}
int main()
{
//freopen("input.txt", "r", stdin);
int iCase;
scanf("%d", &iCase);
while (iCase--) {
scanf("%s", str + 1);//下标从1开始
memset(is_palindromes, -1, sizeof(is_palindromes));
dp[0] = 0;
int len = strlen(str + 1);
for (int i = 1; i <= len; i++) {//遍历下标,i为终止下标
dp[i] = i;
for (int j = 0; j < i; j++) {//查看加上该串是否构成回文串,j为起始坐标
if (Is_palindromes(j + 1, i)) dp[i] = min(dp[i], dp[j] + 1);
}
}
printf("%d\n", dp[len]);
}
return 0;
}
10003 - Cutting Sticks
- 题目大意:一个长为L ( L < 1000 ) 的木块, 有n个切割点 分别为c1, c2, c3, ……, cn (0 < ci < L) 。每次切割的花费是被切木块的长度
求切割完木块的最小花费。 - 思路:
注意不能采用哈夫曼法:选择小木块合并的规则并不是找任意两个最小木块,而是有“相邻”这一条件限制的!
- 我采用了记忆化搜索来实现DP。
- 状态构造:dp[i][j]表示切割第i个切割点和第j个切割点之间的木棍的代价。
- 终态:dp[0][n+1]
- 状态转移方程:dp[i][j] = min{dp[i][k]+dp[k][j]+a[j]-a[i] | i<k<j}
- 初始化边界:dp[i-1][i] = 0(表示当只有一个木棍时代价为0),a[n+1] = l,i>0 && i<=n+1
递归法代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 55;
int l, n, dp[maxn][maxn], a[maxn],cost[maxn][maxn];
int main()
{
while(~scanf("%d", &l), l)
{
memset(dp, 0x3f3f3f3f, sizeof(dp));
memset(cost, 0, sizeof(cost));
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
}
dp[n][n+1] = 0;
a[n+1] = l;
a[0] = 0;
sort(a+1, a+n+1);
for(int i = 0; i <= n; i++)//一个木棍不需要分割
dp[i][i+1] = 0;
for(int len = 2; len <= n+1; len++)//木棍个数
for(int i = 0; i + len <= n+1; i++)//起始切割点下标
{
int j = i + len;//终止切割点下标
for(int k = i+1; k < j; k++)//分割点
dp[i][j] = min(dp[i][j], dp[i][k]+dp[k][j]+a[j]-a[i]);
}
printf("The minimum cutting is %d.\n", dp[0][n+1]);
}
return 0;
}
记忆化搜索代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int INF = 1<<29;
//dp[i][j]表示切割第i个切割点和第j个切割点之间的木棍的最小代价
int dp[55][55], n, l, a[55], x;
int DP(int i, int j)
{
int &ans = dp[i][j];
if(ans>=0) return ans;
ans = INF;
for(int k=i+1; k<j; k++)
{
ans = min(ans, DP(i, k)+DP(k, j)+a[j]-a[i]);//注意a[j]-a[i]要写在括号内
}
return ans;
}
int main()
{
#ifdef ONLINE_JUDGE
#else
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
#endif
while(scanf("%d", &l)!=EOF && l)
{
scanf("%d", &n);
a[0] = 0;
memset(dp, -1, sizeof(dp));
for(int i=1; i<=n; i++)
{
scanf("%d", &a[i]);
dp[i-1][i] = 0;
}
a[n+1] = l;
dp[n][n+1] = 0;
printf("The minimum cutting is %d.\n", DP(0, n+1));
}
return 0;
}
1626 - 括号序列
- 题目大意:
定义如下序列为合法的括号序列:
1、空序列为合法序列
2、若S是合法序列,则(S), [S]也为合法序列
3、若A、B均为合法序列,则AB也为合法序列
题目给出一个括号序列,添加最少的(, ), [, ]使序列合法。 - 思路:
设d(i, j)表示字符串s[i]~s[j]至少添加的括号的数量,则转移如下:- S形如[S’]或(S’),则转移到d(i+1, j-1)
- 如果S至少有两个字符,将其分为AB,转移到min{d(i, j), d(A) + d(B)}
不管是否满足第一条都要尝试第二种转移,因为[][]可能经过第一条转移到][。
打印的时候重新检查一下最优决策。
代码:
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
char s[101];
int dp[101][101];//dp[i][j]表示s[i..j]中至少需要添加几个括号
int n;
int match(char a, char b)
{
return (a=='('&&b==')')||(a=='['&&b==']');
}
void DP()
{
for(int i=0; i<n; i++)
{
dp[i][i] = 1;
dp[i+1][i] = 0;//对应空串
}
for(int i=n-2; i>=0; i--)//起始坐标
{
for(int j=i+1; j<n; j++)//终止坐标
{
dp[i][j] = n;
if(match(s[i], s[j]))
dp[i][j] = min(dp[i][j], dp[i+1][j-1]);
for(int k=i; k<j; k++)
dp[i][j] = min(dp[i][j], dp[i][k]+dp[k+1][j]);
}
}
}
//递归打印
void print(int i,int j)
{
if(i>j) return;
if(i==j)//当只有一个字符单括号
{
if(s[i]=='('||s[i]==')')
printf("()");
else
printf("[]");
return;
}
int ans=dp[i][j];
if(match(s[i],s[j])&&ans==dp[i+1][j-1])//当有双括号
{
printf("%c",s[i]);print(i+1,j-1);printf("%c",s[j]);
return;
}
for(int k=i;k<j;k++)
if(ans==dp[i][k]+dp[k+1][j])
{
print(i,k);print(k+1,j);
return;
}
}
int main()
{
int T;
scanf("%d", &T);
getchar();
while(T--)
{
fgets(s, 101, stdin);
n = strlen(s)-1;
//cout << n << endl;
memset(dp, -1, sizeof(dp));
DP();
print(0, n-1);
printf("\n");
if(T) printf("\n");
//fgets(s, 101, stdin);
}
return 0;
}
10285 - Longest Run on a Snowboard
题目链接:10285 - Longest Run on a Snowboard
- 题目大意:滑雪问题,在一个矩阵上寻找最长递减路径长度。
- 思路:当无法确定循环方向时,记忆化搜索比较简单。设dp[i][j]表示从第i行第j列出发可以达到的最长路径,则有:
dp[i][j]=max{dp[i-1][j],dp[i+1][j],dp[i][j-1],dp[i][j+1]}+1 (前提是从(i,j)点要可以走到相邻的那个点,即只有相邻点比点(i,j)低才可以转移)
代码:
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
char name[1000];
int G[105][105], dp[105][105], n, m;
int dx[5] = {0, 0, 1, -1};
int dy[5] = {1, -1, 0, 0};
int DP(int i, int j)
{
int &ans = dp[i][j];
if(ans>0) return ans;
ans = 1;
for(int k=0; k<4; k++)
{
int x = dx[k]+i, y = dy[k]+j;
if(x<1 || x>n || y<1 || y>m) continue;
if(G[x][y]<G[i][j])
ans = max(ans, DP(x, y)+1);
}
return ans;
}
int main()
{
int T;
scanf("%d", &T);
while(T--)
{
scanf("%s%d%d", name, &n, &m);
for(int i=1; i<=n; i++)
{
for(int j=1; j<=m; j++)
{
scanf("%d", &G[i][j]);
}
}
memset(dp, 0, sizeof(dp));
int Max = 0;
for(int i=1; i<=n; i++)
{
for(int j=1; j<=m; j++)
{
Max = max(Max, DP(i, j));
}
}
printf("%s: %d\n", name, Max);
}
return 0;
}
10118 - Free Candies
题目链接:10118 - Free Candies
- 题目大意:桌上有4堆糖果,每堆有N(N≤40)颗。佳佳有一个最多可以装5颗糖的小篮子。他每次 选择一堆糖果,把最顶上的一颗拿到篮子里。如果篮子里有两颗颜色相同的糖果,佳佳就把 它们从篮子里拿出来放到自己的口袋里。如果篮子满了而里面又没有相同颜色的糖果,游戏 结束,口袋里的糖果就归他了。当然,如果佳佳足够聪明,他有可能把堆里的所有糖果都拿 走。为了拿到尽量多的糖果,佳佳该怎么做呢?
- 思路:该题目主要难点在状态构造。并且记忆化搜索方法的技巧性很强。
- 状态构造:dp[a][b][c][d]表示四个堆已经分别拿了a、b、c、d个后,可以获得的糖果数目。
- 终态:篮子中有5个颜色不同的糖果时,能获得的最大糖果数。
- 状态转移方程:以第一堆为例,状态转移方程为:dp(a,b,c,d)=dp(a+1,b,c,d) (如果拿掉第一堆的第a+1个不会产生相同颜色)、dp(a,b,c,d)=dp(a+1,b,c,d)+1 (如果拿掉第一堆的第a+1个会产生相同颜色)。
- 初始化:dp[][][][] = -1.
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <set>
using namespace std;
int num[45][5], n;
int vis[25], top[5];//vis记录某个颜色是否在篮子里,top记录每一堆已经拿了几个
int dp[45][45][45][45];
int DP(int k)
{
int &ans = dp[top[0]][top[1]][top[2]][top[3]];
if(ans>0) return ans;
ans = 0;//一定在判断k之前赋值为0
if(k>=5) return ans;//超出时可获得0个
for(int i=0; i<4; i++)
{
if(top[i]==n) continue;//该堆已经拿完
if(vis[num[top[i]][i]])//该颜色重复
{
vis[num[top[i]][i]] = 0;
top[i]++;
ans = max(ans, DP(k-1)+1);
top[i]--;
vis[num[top[i]][i]] = 1;
}
else
{
vis[num[top[i]][i]] = 1;
top[i]++;
ans = max(ans, DP(k+1));
top[i]--;
vis[num[top[i]][i]] = 0;
}
}
return ans;
}
int main()
{
while(scanf("%d", &n)!=EOF && n)
{
for(int i=0; i<n; i++)
{
for(int j=0; j<4; j++)
{
scanf("%d", &num[i][j]);
}
}
memset(vis, 0, sizeof(vis));
memset(dp, -1, sizeof(dp));
memset(top, 0, sizeof(top));
printf("%d\n", DP(0));
}
return 0;
}
1629 - Cake slicing
题目链接:1629 - Cake slicing
- 题目大意:一块n*m的矩形蛋糕,有k个草莓,现在要将蛋糕切开使每块蛋糕上都恰有一个(这意味着不能切出不含草莓的蛋糕块)草莓,要求只能水平切或竖直切,求最短的刀切长度。
- 思路:本题不是特别难,一旦想到使用DP法,就很容易想到状态的构造和状态转移方程。
- 状态的构造:dp[x1][y1][x2][y2]表示左上角为(x1, y1),右下角为(x2, y2)的矩形的切割最大长度和。
- 终态:dp[1][1][n][m]
- 状态转移方程:当矩形内有多个草莓时,dp[x1][y1][x2][y2] = min(dp[x1][y1][x2][y2], DP(x1, y1, i, y2)+DP(i+1, y1, x2, y2)+y2-y1+1)(刀平行于y轴切); dp[x1][y1][x2][y2] = min(dp[x1][y1][x2][y2], DP(x1, y1, x2, j)+DP(x1, j+1, x2, y2)+x2-x1+1);(刀平行于x轴切)
- 初始化:dp[][][][] = -1.
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int INF = 1<<29;
int n, m, k;
int dp[21][21][21][21];
int num[21][21][21][21];//存储该矩形内有多少个草莓
struct Node
{
int x, y;
};
Node c[500];
int check(int x1, int y1, int x2, int y2)
{
if(num[x1][y1][x2][y2]) return num[x1][y1][x2][y2];
int cnt = 0;
for(int i=0; i<k; i++)
{
if(c[i].x>=x1 && c[i].x<=x2 && c[i].y>=y1 && c[i].y<=y2) cnt++;
}
return num[x1][y1][x2][y2]=cnt;
}
int DP(int x1, int y1, int x2, int y2)
{
if(check(x1, y1, x2, y2)<2) return 0;
int &ans = dp[x1][y1][x2][y2];
if(ans!=-1) return ans;
ans = INF;
for(int i=x1; i<x2; i++)
{
if(check(x1, y1, i, y2)<1 || check(i+1, y1, x2, y2)<1) continue;
ans = min(ans, DP(x1, y1, i, y2)+DP(i+1, y1, x2, y2)+y2-y1+1);
}
for(int j=y1; j<y2; j++)
{
if(check(x1, y1, x2, j)<1 || check(x1, j+1, x2, y2)<1) continue;
ans = min(ans, DP(x1, y1, x2, j)+DP(x1, j+1, x2, y2)+x2-x1+1);
}
return ans;
}
int main()
{
#ifdef ONLINE_JUDGE
#else
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
#endif
int kase = 1;
while(cin >> n >> m >> k)
{
for(int i=0; i<k; i++)
{
cin >> c[i].x >> c[i].y;
}
memset(dp, -1, sizeof(dp));
memset(num, 0, sizeof(num));
printf("Case %d: %d\n", kase++, DP(1, 1, n, m));
}
return 0;
}