动态规划入门(数字三角形、DAG模型、最佳加法表达式、经典取石子游戏)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Tianweidadada/article/details/82919228

1、普通dp

POJ-1163 数字三角形

递推式:

dp[i] = max(dp[i+1][j],dp[i+1][j+1]) + a[i][j]

递推法:

#include<iostream>
#include<cstring>
using namespace std;

int dp[105][105]; // dp[i][j] 表示 从(i,j) 到三角形底部 最大和 
int a[105][105];//三角形值 
int main()
	{
		int n;
		while(cin >> n){
			memset(dp,0,sizeof(dp));
			memset(a,0,sizeof(a));
			for(int i = 0; i < n; ++i){
				for(int j = 0; j < i+1; ++j){
					cin >> a[i][j];
				}
			}
			for(int j = 0; j < n; ++j){
				dp[n][j] = 0; // 边界值 
			}
			for(int i = n-1; i >= 0; -- i){
				for(int j = 0; j < i+1; ++j){
					dp[i][j] = max(dp[i+1][j],dp[i+1][j+1]) + a[i][j];
				}
			}
			cout << dp[0][0] << endl;
		}		
		
		
		return 0;	
	} 

记忆化搜索:

#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;

int dp[105][105];
int a[105][105];
int n;

int rec(int i, int j){
	if(dp[i][j] >= 0)
		return dp[i][j];
	if(i == n)
		return dp[i][j] = 0;
	return dp[i][j] = max(rec(i+1,j),rec(i+1,j+1)) + a[i][j];
}
/*
	记忆化搜素 
*/ 
int main()
	{
		while(cin >> n){
			memset(a,0,sizeof(a));
			memset(dp,-1,sizeof(dp));
			for(int i = 0; i < n; ++i){
				for(int j = 0; j < i+1; ++j){
					cin >> a[i][j];
				}
			}	
			cout << rec(0,0) << endl;
		}
		
		return 0;	
	} 

2、硬币问题(固定起点终点的DAG模型

n种硬币,面值分别为V$_1,V$_2,V$_3,...V$_n,每种都有无限多。给定非负整数S,可以选用多少个硬币,使得面值之和恰好为S

输出硬币数目的最小值和最大值。1 \leq n \leq 100, 0 \leq S \leq 10000, 1 \leq V$_i \leq S

分析:本质为DAG上的路径问题。将每种面值看做一个点,表示“还需要凑足的面值”,则初始态为S,目标态为0。若在当前状态i,每使用一个硬币j,状态转移到 i-V$_j。本题为限制了起点终点的DAG,即起点必须为S, 终点必须为0。

状态转移方程(最长路):

dp[i] = max(dp[i-v[j]] + 1,dp[i])

递推:

#include<iostream>
#include<cstring>
using namespace std;
const int INF = (1 << 30); 
const int N = 100;
int v[N];
int S,n;
int minv[N];
int maxv[N];
int min_coin[N];//记录 最短 路径 
int max_coin[N];// 记录最长 路径 

void print_path(int *d, int S){
	while(S){
		cout << d[S] + 1 <<" "; // d[S] 代表当前取的元素序号 0-indexed  + 1 转化为 1-indexed 
		S -= v[d[S]];
	}
}
/*
	用 min_coin / max_coin 记录路径 用空间换时间 
*/ 
int main()
	{
		while(cin >> n >> S){
			fill(minv,minv+N,INF);//初始化 
			fill(maxv,maxv+N,-INF);
			memset(v,0,sizeof(v));
			minv[0] = 0; //这里是恰好取完 
			maxv[0] = 0;
			for(int i = 0; i < n; ++i){
				cin >> v[i];
			}
			for(int i = 1; i <= S; ++i){
				for(int j = 0; j < n; ++j){
					if(i >= v[j]){
						if(minv[i] > minv[i-v[j]] + 1){
							minv[i] = minv[i-v[j]] + 1;
							min_coin[i]  = j; // 当前 金钱总额为 i时候取第j个硬币 
						}
						if(maxv[i] < maxv[i-v[j]] + 1 ){
							maxv[i] = maxv[i-v[j]] + 1;
							max_coin[i] = j;
						}
					}
				}
			}
			print_path(min_coin,S);	//打印路径 
		}
		
		return 0;
	}

记忆化搜索:

#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;

const int N = 100;
bool vis[N];//标记 某个状态是否被访问 
int v[N];// 硬币面额 
int n,S;
int dp[N]; // dp[i] 表示 总额为 i 最多能用的硬币数 
 
int rec(int S){
	if(vis[S]) // 表示已经访问S 
		return dp[S];
	int & ans = dp[S];  // 用ans 引用dp[S] 
	vis[S] = true;
	
	ans = -(1 << 30); //记录无解情况 
	for(int i = 0; i < n; ++i){
		if(S >= v[i]){
			ans = max(ans,rec(S-v[i]) + 1); // DAG中的 dp[i] = dp[j] + 1  (j,i) belongs to E 注意 与 (i,j) belongs to E 区分 
		}
	}
	return ans;
}


void print_path(int *d, int S){// 按照最小字典序打印结果 
	for(int i = 0; i < n; ++i){
		if(S >= v[i] && dp[S] == dp[S-v[i]] +1){
			cout << i << " ";
			print_path(d,S-v[i]);
			break; 
		} 
	}	
}

int main()
	{
		while(cin >> n >> S){
			memset(dp,-1,sizeof(dp));
			memset(vis,false,sizeof(vis));
			dp[0] = 0;
			vis[0] = true;// 注意 这个 设置为已访问 
			for(int i = 0; i < n; ++i){
				cin >> v[i];
			}
			cout << rec(S) << endl;	
			print_path(dp,S);
		}
		
		return 0;
	}

3、简单区间dp(寻找定量)

最佳加法表达式

题目:

有一个由1..9组成的数字串。问如果将m个加号插入到这个数字串中,使得所形成的表达式值最小。

分析:

这一题的定量是什么?可以发现添加加号后,表达式最后一定是个数字,这就是定量。从这里入手,不难发现可以把以前状态认为是在前 i个字符中插入k-1(k \leq m)个加号(这里i 当做决策在枚举),然后i+1 到最后一位是一个没有分割的数字串,第k 个加号就插在 i 与i+1 之间,这样就构造出了整个数字串的最优解。至于前i 个字符插入 k-1 个加号,又回到了原问题的形式。

所以状态转移方程:

                                 dp[i][j] = min(dp[i][j],dp[k][j-1] + num(k+1,i))
其中 :

                                 dp[i][0] = num(1,i)  

code:

#include<iostream>
#include<stdio.h>
#include<vector>
#include<map>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<set>
#include<cmath>
using namespace std;

char str[100];
int a[100];
int dp[100][100]; // dp[i][j]  在 前i个字符间插入 j 个"➕" 能得到的最小值
int path[100][100];

int num(int st, int en){
    int ans = 0;
    for(int i = st; i <= en; ++i)
        ans = ans*10 + a[i];
    return ans;
}
/*
    最佳加法表达式
*/
int main()
    {
        int m;
        scanf("%d",&m);
        getchar();
        scanf("%s",str);
        int len = strlen(str);
        for(int i = 1; i <= len; ++i){
                a[i] = str[i-1]-'0';
        }

        fill(dp[0],dp[0]+100*100,1000);
        for(int i = 1; i <= len; ++i){
            dp[i][0] = num(1,i); // 前i个字符不添加 加号 为前i个字符表示的数字
        }
        for(int i = 2; i <= len; ++i){ //阶段
            for(int j = 1; j <= m; ++j){
                for(int k = i-1; k >= 1; --k){ // 在第 k位置之后 插入 加号
                    if(dp[i][j] > dp[k][j-1] + num(k+1,i)){
                        dp[i][j] = dp[k][j-1] + num(k+1,i);
                        path[i][j] = k;
                    }
                }
            }
        }
        vector<int> pos;
        int i = len,j = m;
        while(path[i][j] && j > 0){
            int k = path[i][j];
            pos.push_back(k);
            j--;
            i = k;
        }
        for(int i = 1; i <= len; ++i){
            printf("%d",a[i]);
            for(int j = 0; j < pos.size(); ++j){
                if(i == pos[j]){
                    printf("+");
                }
            }
        }
        printf(" = %d\n",dp[len][m]);

        return 0;
    }

4、石子合并(区间dp)

NYOJ 737

题目:

    有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值。

样例输入

3
1 2 3
7
13 7 8 16 21 4 18

输出;

9

239

注意与 最佳表达式 区分, 这里合并次数为 n-1 并且只有相邻的石子可以合并。这里当前状态 dp[i][j] 表示,从 第 i 到第 j  个石子 花费的最小值。

则 状态转移为:

dp[i][j] = min(dp[i][j],dp[i][k] + dp[k+1][j] + cost(i,j)) 

意思是在 i 与 j 这个区间之间找到 一个 合并点 k 使得 dp[i][j]  达到最小 此时 dp[1][n]  即为最终的 最小花费。

其中:

dp[i][i] = 0 $ (i \in 1...n)

code:

递推:

#include<bits/stdc++.h>
using namespace std;
const int N = 200+5;
int sum[N];
int dp[N][N];
int a[N];
int main()
    {
        int n;
        while(scanf("%d",&n) != EOF){
            memset(dp,0x3f3f3f,sizeof(dp));
            sum[0] = 0;
            for(int i = 1; i <= n; ++i){
                scanf("%d",&a[i]);
                sum[i] = sum[i-1] + a[i];
            }
            for(int i = 1; i <= n; ++i){
                dp[i][i] = 0; // 注意这里 初始化的意义
            }
            for(int len = 2; len <= n; ++len){ // 遍历合并的长度
                for(int i = 1; i <= n-len+1; ++i){
                    int j = i+len-1;
                    for(int k = i; k < j; ++k){
                        dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j] + sum[j]-sum[i-1]);
                    }
                }
            }
            printf("%d\n",dp[1][n]);
        }

        return 0;
    }

记忆化搜索:

#include<iostream>
#include<stdio.h>
#include<vector>
#include<map>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<set>
#include<cmath>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 205;
int sum[N];
int a[N];
int dp[N][N];

int rec(int i, int j){
    if(dp[i][j] != INF){
        return dp[i][j];
    }
    int & ans = dp[i][j];
    for(int k = i; k < j; ++k){
        ans = min(ans,rec(i,k)+rec(k+1,j) + sum[j]-sum[i-1]);
    }
    return ans;
}

int main()
    {
        int n;
        while(scanf("%d",&n)!=EOF){
            //fill(dp[0],dp[0]+N*N,INF);
            memset(dp,INF,sizeof(dp));
            for(int i = 1; i <= n; ++i){
                dp[i][i] = 0;
            }
            sum[0] = 0;
            for(int i = 1; i <= n; ++i){
                scanf("%d",&a[i]);
                sum[i] = sum[i-1] + a[i];
            }

            printf("%d\n",rec(1,n));
        }

        return 0;
    }

5、Tom的烦恼(dp前的预处理——排序)

Problem

Tom是一个非常有创业精神的人,由于大学学的是汽车制造专业,所以毕业后他用有限的资金开了一家汽车零件加工厂,专门为汽车制造商制造零件。由于资金有限,他只能先购买一台加工机器。现在他却遇到了麻烦,多家汽车制造商需要他加工一些不同零件(由于厂家和零件不同,所以给的加工费也不同),而且不同厂家对于不同零件的加工时间要求不同(有些加工时间要求甚至是冲突的,但开始和结束时间相同不算冲突)。

Tom当然希望能把所有的零件都加工完,以得到更多的加工费,但当一些零件的加工时间要求有冲突时,在某个时间内他只能选择某种零件加工(因为他只有一台机器),为了赚得尽量多的加工费,Tom不知如何进行取舍。

现在请你帮Tom设计一个程序,合理选择部分(或全部)零件进行加工,使得得到最大的加工费。

Input

第一行是一个整数n(n<=30000),表示共有n个零件须加工。

接下来的n行中,每行有3个整数,分别表示每个零件加工的时间要求。

第一个表示开始时间,第二个表示该零件加工的结束时间,第三个表示加工该零件可以得到的加工费。

注:数据中的每个数值不会超过100000.

Output

输出一个整数,表示Tom可以得到的最大加工费。

Sample Input

3

1  3  10

4  6  20

2  5  25

 

Sample Output

30

首先对数据按照结束时间先后进行排序,这里类似贪心里面的“活动安排问题“,但是不同点在于 活动安排 只考虑 个数,不需要考虑权重,所以这里不可以用贪心算法。

dp[i]  为当前状态,表示取最后一个零件为第i个时候,得到的最大费用。

则状态转移方程:

dp[i] = max(dp[i],dp[j] + w[i]) $ (end[j] \leq start[i])

code:

#include<iostream>
#include<stdio.h>
#include<vector>
#include<map>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<set>
#include<cmath>
using namespace std;
typedef long long LL;
int n;
const int N = 30000+5;
struct Cop{
    int st,en,w;
    Cop(){}
    Cop(int pst,int pen,int pw):st(pst),en(pen),w(pw){};
    bool operator<(const Cop o)const{
        if(en == o.en)  //结束时间相同 时候 按照 开始时间晚的排在后面
            return st > o.st;
        return en < o.en;
    }
};

Cop cops[N];

int dp[N]; // dp[i] 最后一个取第 i个物品 可以获得的最大值
int main()
    {
        while(scanf("%d",&n) != EOF){
            int st,en,w;
            memset(dp,0,sizeof(dp));
            for(int i = 1; i <= n; ++i){
                scanf("%d%d%d",&st,&en,&w);
                cops[i] = Cop(st,en,w);
            }
            sort(cops,cops+n);
            for(int i = 1; i <= n; ++i)
                dp[i] = cops[i].w; // 初始化

            int ans = -1;
            for(int i = 2; i <= n; ++i){
                for(int j = 1; j < i; ++j){
                    if(cops[j].en <= cops[i].st){ // 前面 结束时间小于当前 i 起始时间 则可以 
                        dp[i] = max(dp[i],dp[j] + cops[i].w);
                    }
                }
                ans = max(ans,dp[i]); //记录最大的
            }
            printf("%d\n",ans);
        }

        return 0;
    }

参考:

https://jingyan.baidu.com/article/4b52d702df537efc5c774bc9.html  Latex 符号

https://wenku.baidu.com/view/7aca6f3e0912a216147929ab.html  浙大 acm ppt

https://blog.csdn.net/fuyukai/article/details/43793863   区间 dp入门

猜你喜欢

转载自blog.csdn.net/Tianweidadada/article/details/82919228