算法竞赛入门经典(第2版)—第九章(动态规划)

文章目录

        • 零碎知识点
          • 递推法和记忆化搜索法的思考
        • 题目
          • 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 
#include 
#include 
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 
#include 
#include 
#include 
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
#include
#include
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 
#include 
#include 
#include 
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

题目链接: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 
#include 
#include 
#include 
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 
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 
#include 
#include 
#include 
#include 
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 
#include 
#include 
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 

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

题目链接: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
    • 初始化边界:dp[i-1][i] = 0(表示当只有一个木棍时代价为0),a[n+1] = l,i>0 && i<=n+1

递归法代码:

#include 
#include 
#include 
#include 
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 
#include 
#include 
#include 
#include 
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 
#include 
#include 
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 
#include 
#include 
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 
#include 
#include 
#include 
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 
#include 
#include 
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;
}

你可能感兴趣的:(算法竞赛入门经典(第2版))