基础算法系列总结:动态规划(解公司外包成本问题)
理论辅助:
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
上面这段定义文字是我Copy过来的,目前我还没有那么好的总结能力,呵呵,如果大家对于动态规划不是很熟悉的话,可能看着上面一段文字会不知所云,我一般的学习方法是首先扫描一下基本定义,不深究(有点不求甚解的味道),然后去看一些实例,结合自己的体会,最后再回顾,精读一下定义,这样我对定义才能够真正的理解。下面我们依托一个经典的算法问题来体现上面这段文字的思想,0-1背包问题在算法学习中可谓是必修课程,一般在讲动态规划问题的时候都会用到这个例子。
问题描述:
一个旅行者有一个最多能用M公斤的背包,现在有N件物品,
它们的重量分别是W1,W2,...,Wn,
它们的价值分别为P1,P2,...,Pn.
若每种物品只有一件 在不超过M公斤的前提下,求旅行者能获得最大总价值的方案。
输入格式:
M,N
W1,P1
W2,P2
......
问题分析:
经过我们粗略的扫描动态规划的定义,我们了解到动态规划问题基本都是通过建立表格,填表格来解决问题的,这里也不例外。
首先我们需要确定表格内部单元格的逻辑关系。
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[i][j]表示前i件物品恰放入一个容量为j的背包可以获得的最大价值。则其状态转移方程便是:
f[i][j] = max{f[i-1][j], f[i-1][j-w[i]]+P[i]}
为什么这里会出现max呢?因为只能从两个状态而来,也就是取和不取当前物品。 我在前i件物品取得重量不超过j的最大价值,是由取不取第i件物品而得到的。对于【将前i件物品放入容量为v的背包中】这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为的背包中”,价值为f[i-1][j];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为j-w[i]的背包中”,此时能获得的最大价值就是f[i-1][j-w[i]]再加上通过放入第i件物品获得的价值P[i]。
下面我们通过一组真实的数据来过一遍算法流程:
测试数据:
10,3
3,4
4,5
5,6
上面这幅图将f[i][j] = max{f[i-1][j], f[i-1][j-w[i]]+P[i]} 这个式子表现得淋漓尽致..动态规划问题基本都是像这样通过建立表格,填表格来解决问题的。
方案代码:
代码如下(为了能够让更多的人可以阅读代码,采用C语言表达):
int c[10][100];
int knapsack(int m,int n)
{
int i,j,w[10],p[10];
for(i=1;i<n+1;i++)
scanf("\n%d,%d",&w[i],&p[i]);
for(i=0;i<10;i++)
for(j=0;j<100;j++)
c[i][j]=0;
for(i=1;i<n+1;i++)
for(j=1;j<m+1;j++)
{
if(w[i]<=j)
{
if(p[i]+c[i-1][j-w[i]]>c[i-1][j])
c[i][j]=p[i]+c[i-1][j-w[i]];
else
c[i][j]=c[i-1][j];
}
else c[i][j]=c[i-1][j];
}
return(c[n][m]);
}
int main()
{
int m,n;int i,j;
scanf("%d,%d",&m,&n);
printf("Input each one:\n");
printf("%d",knapsack(m,n));
printf("\n");
for(i=0;i<n+1;i++)
for(j=0;j<m+1;j++)
{
printf("%2d ",c[i][j]);
if(j==m)printf("\n");
}
}
算法实践:
问题描述:
Zealot Yin 所在的X公司需要至少W个其他公司提供的外包人员,现在有N家公司向X公司提供了可选方案,其中
P_i代表可提供外包人员单位数,如5人为一个单位数,若选用该公司方案,则必须采用整单位数的人数,如5人为一个单位数,则X公司只能采用n*5个人数(n=0,1,2,….)。C_i代表为P_i单位数员工提供的总工资,单位是万元
输入格式:
N W
P_i C_i
请问,采取什么样的方案可以满足X公司花最少的开销招到至少W个外包人员,求出最少的开销金额。
例如X公司需要至少15个外包员工,A公司的价格方案是 2万元/每三人,B公司的价格方案是 3万元/每五人。
输入方案为
2 15
3 2
5 3
得到的最少的开销金额为9万元
分析问题:
这是一个求最优解的问题,与上面的背包问题有些类似,遇到这样的问题我会首先想到使用动态规划的方法来解决问题。同样我们这里建表格来解决问题,这里我们选用一个数组来模拟表格,因为我们在每次输入一个公司的方案的时候就会进行计算当前的最优解,所以我们的建表的承载数据结构只需要一个简单的一维数组就可以解决了。同时数组dp[i]也是至少招i个人员的最佳方案。就像上面的输入数据例子,我们不单只是找出了招15人的解决方案,15人以下的所有最优方案都已经找出来了。也就是说当我们输入3 2的时候,dp数组的最终值的情况如下,其中黑桃表示在当前情况下不可解,a[3]可以认为是代表招3人情况下的最优解,当然a[4]在当前下就是不可解的,这点还是比较好理解。
当输入5 3后,我们dp数组的情况如下:
方案代码:
#define MAX 1000000000
int dp[55005];
int min(int x,int y) { return x<y?x:y; }
int main()
{
int n,h;
int i,j;
int p,c;
while(scanf("%d%d",&n,&h)==2)
{
dp[0] = 0;
for ( i = 1 ; i <= h ; i ++)
{
dp[i] = MAX;
}
for( i = 0 ; i < n ; i++)
{
scanf("%d%d",&p,&c);
for(j = 0 ; j<=h ;j++)
{
if( j + p > h ) dp[h] = min (dp[h] , dp[j] + c );
else dp[ j + p ] = min ( dp[j+p] , dp[j] + c );
char disstr="x";
printf("\n");
for(int x=0;x<=h;x++)
{ if(dp[x]==1000000000)
printf("%2c ",disstr);
else
printf("%2d ",dp[x]);
}
}
printf("\n");
}
printf("The Min Value Is %d\n",dp[h]);
}
return 0;
}
算法思想总结:
动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果,与贪婪算法不同的是,在贪婪算法中,每采用一次贪婪准则,便做出一个不可撤回的决策;而在动态规划算法中,还要考察每个最优决策序列中是否包含一个最优决策子序列,即问题是否具有最优子结构性质。
算法系列目录:
1.递归与分治