POJ 1651 - Multiplication Puzzle(区间DP,矩阵链乘三种模版详解)

来源:互联网 发布:jira7 windows 破解版 编辑:程序博客网 时间:2024/06/01 09:18

【题意】题目链接http://poj.org/problem?id=1651 

给出n(<=100)个数字,每一个数字有一个权值Ai,求从中抽出除了第一个和最后一个以外的中间的数字的最小费用。抽取的顺序不同最后总的费用也不同,每次抽取第i个数字的费用为a[i-1]*a[i]*a[i+1];例如:有5个数字分别为10 1 50 20 5;

如果抽出的顺序为1->20->50的话总费用为

10*1*50 + 50*20*5 + 10*50*5 = 500+5000+2500 = 8000

如果抽出的顺序为50->20->1的话总费用为

1*50*20 + 1*20*5 + 10*1*5 = 1000+100+50 = 1150

【分析】有三种方法:

方法一:用dp[i][j]表示区间(数组下标)i~j之间所有数字全部抽出后的最小费用(最后留下i和j);k表示i~j之间最后一个抽出的数字。

则有状态转移方程:dp[i][j] = min{dp[i][j] , dp[i][k] + dp[k][j] + a[i]*a[k]*a[j]};就是找到区间i~j之间最后一个取出哪个数字费用最小。

其中区间i~k和区间k~j之间的最小费用都已经算出,所以直接加上最后一次取出k的费用a[i]*a[k]*a[j]就是当前的总费用。

细节请看代码注释。

 

方法二:把他看成白书上的最优矩阵链乘,用dp[i][j]表示区间(数组下标)i-1~j之间所有数字全部抽出后的最小费用(最后只留下j,因为i-1不在区间i~j内);

k表示i-1~j之间最后一个抽出的数字;则有状态转移方程:dp[i][j] = min{dp[i][j] , dp[i][k]+dp[k+1][j] + a[i-1]*a[k]*a[j]};就是找到区间i-1~j之间最后一个取出哪个数字费用最小

这里为什么加上dp[k+1][j]而不是dp[k][j]呢?因为这个dp[i][j]是抽出i-1~j之间的所有数后的最小费用,关键是i会被抽出也就是这里的k会被取了,如果写成dp[k][j]的话就相当于把k已经取出了,而这里是正在计算取哪个k费用最少,k是还没被取出的。方案一中的dp[k][j]k是不被取出的.然后再加上a[i-1]*a[k]*a[j]就是当前的总费用;注意:a[i-1]和a[j]都是没被取出的。

请细细品味一下两种状态dp[][]的含义就能明白。

 

方法三:用记忆化搜索,由于本人刚刚学DP,很难控制递推的边界,无奈只能用记忆化最顺手,dp[i][j]和dfs(i,j)都表示i~j之间的所有数字全部抽出后的最小费用,具体看代码

 

这三种方法都是最优矩阵链乘的模版 

【方法一AC代码】 0ms

#include <cstdio>#include <cstring>#include <algorithm>using namespace std;#define INF 0x3f3f3f3f#define MAXN 110int dp[MAXN][MAXN], a[MAXN];//d[i][j]表示区间i~j之间所有数字去掉后的最小费用,k表示i~j之间最后一个抽出的数字int main (){#ifdef SHYfreopen("e:\\1.txt","r",stdin);#endifint n;while(~scanf ("%d%*c", &n)){for (int i = 0; i < n; i++)scanf ("%d%*c", &a[i]);memset(dp,0,sizeof(dp));/*for(int i = 0; i < n-2; i++)dp[i][i+2] = a[i]*a[i+1]*a[i+2];这个是对只有三个数字时候的边界初始化添加这段初始化的话可以把下面的len=2改成len=3了*/for (int len = 2; len < n; len++)//len是区间包含的长度{/*枚举区间起点,不需要到n-len,因为n点右边没数字,也就是说只有两个点,不需要相乘  不然会乘上a[n]是个无效数字*/for(int i = n-1-len; i >= 0; i--)//i顺序可以换{int j = i+len;//区间终点//找到区间i~j之间最后一个取出哪个数字费用最小dp[i][j] = INF;for (int k = i+1; k < j; k++)dp[i][j] = min(dp[i][j], dp[i][k]+dp[k][j]+a[i]*a[k]*a[j]);}}printf ("%d\n", dp[0][n-1]);}return 0;}


 

【方法二AC代码】 0ms

#include <cstdio>#include <cstring>#include <algorithm>using namespace std;#define INF 0x3f3f3f3f#define MAXN 110int dp[MAXN][MAXN], a[MAXN];//d[i][j]表示区间i-1~j所有数字全部去掉后的最小费用,k表示i-1~j之间最后一个抽出的数字int main (){#ifdef SHYfreopen("e:\\1.txt","r",stdin);#endifint n;while(~scanf ("%d%*c", &n)){for (int i = 0; i < n; i++)scanf ("%d%*c", &a[i]);memset(dp,0,sizeof(dp));/*for(int i = 1; i < n-1; i++)dp[i][i+1] = a[i-1]*a[i]*a[i+1];这个是对只有三个数字时候的边界初始化添加这段初始化的话可以把下面的len=2改成len=3了*/for (int len = 2; len < n; len++)//len是区间包含的长度{/*枚举区间起点,不需要到0,因为0点左边没数字,也就是说只有两个点,不需要相乘  不然会乘上a[-1]是个无效数字*/for(int i = n-len; i >= 1; i--)//i顺序可以换{int j = i+len-1;//区间终点//找到区间i-1~j之间最后一个取出哪个数字费用最小dp[i][j] = INF;for (int k = i; k < j; k++)dp[i][j] = min(dp[i][j], dp[i][k]+dp[k+1][j]+a[i-1]*a[k]*a[j]);}}printf ("%d\n", dp[1][n-1]);}return 0;}



 【方法三AC代码】 0ms

#include <cstdio>#include <cstring>#include <algorithm>using namespace std;#define MAXN 110#define INF 0x3f3f3f3fint a[MAXN], dp[MAXN][MAXN];//dp[i][j]和dfs(i,j)都表示i~j之间的所有数字全部抽出后的最小费用int dfs(int i, int j){    if (-1 != dp[i][j])//记忆化        return dp[i][j];    if (j-i <= 1)//当区间数字不到3个时费用为0,不能抽出数字        return dp[i][j] = 0;    if (j-i == 2)//当区间数字正好有三个的时候抽出中间的数字        return dp[i][j] = a[i]*a[i+1]*a[i+2];    //当区间数字大于3个的时候,枚举i~j之间抽出第k个数的总费用,取最小值    int ans = INF;    for (int k = i+1; k < j; k++)        ans = min(ans, dfs(i,k)+dfs(k,j)+a[i]*a[k]*a[j]);    return dp[i][j] = ans;}int main (){    #ifdef SHY        freopen("e:\\1.txt", "r", stdin);    #endif    int n;    while(~scanf ("%d%*c", &n))    {        for (int i = 0; i < n; i++)            scanf ("%d%*c", &a[i]);        memset(dp,-1,sizeof(dp));        printf ("%d\n", dfs(0,n-1));    }    return 0;}


 


 

0 0
原创粉丝点击