挑战程序设计(算法和数据结构)—动态规划(DP)

斐波那契数列

题目11.2链接Fibonacci Number
解决斐波那契数列有三种方法
1>直接递归

fib(n)
	if n==0 || n==1
		return 1
	return fib(n-1)+fib(n-2)

2>记忆化递归

fib(n)
	if n==0 || n==1
		return F[n] = 1
	if F[n]
		return F[n]
	return F[n] = fib(n-1)+fib(n-2)

3>动态规划

fib()
	F[0] = 1
	F[1] = 1
	for i = 2-n
		F[i] = F[i-1]+F[i-2]

代码如下:

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

const int Max = 50;
int F[Max];

int Fib(int n)
{
    F[0] = 1;
    F[1] = 1;
    if(n==0 || n==1) return F[n];
    if(F[n])
        return F[n];
    return F[n] = Fib(n-1)+Fib(n-2);
}
void makeFib()
{
    F[0] = 1;
    F[1] = 1;
    for(int i=2; i<Max; i++)
    {
        F[i] = F[i-1]+F[i-2];
    }
}
int main()
{
    int n;
    cin >> n;
    memset(F, 0, sizeof(F));
    cout << Fib(n) << endl;
}
最长公共子列(LCS)

题目11.3链接Longest Common Subsequence
解析:可以设置以下变量

变量含义
c[m+1][n+1]c[i][j]表示 X i 与 Y j X_{i}与Y_{j} XiYj的LCS长度

X i 与 Y j X_{i}与Y_{j} XiYj的LCS时要考虑两种情况

  • x m = y n x_{m}=y_{n} xm=yn时, X m − 1 与 Y n − 1 X_{m-1}与Y_{n-1} Xm1Yn1的LCS需要加一
  • x m ! = y n x_{m}!=y_{n} xm!=yn时,取 X m 与 Y n − 1 X_{m}与Y_{n-1} XmYn1的LCS和 X m − 1 与 Y n X_{m-1}与Y_{n} Xm1Yn的LCS的更大者

c [ i ] [ j ] = { 0 , i = 0 o r j = 0 c [ i − 1 ] [ j − 1 ] + 1 , i , j &gt; 0 a n d x i = y j m a x ( c [ i ] [ j − 1 ] , c [ i − 1 ] [ j ] ) , i , j &gt; 0 a n d x i ! = y j c[i][j] = \begin{cases} 0 &amp; &amp; ,i=0 or j=0 \\ c[i-1][j-1]+1 &amp; &amp; ,i,j&gt;0 and x_{i}=y_{j}\\ max(c[i][j-1], c[i-1][j]) &amp; &amp; ,i,j&gt;0 and x_{i}!=y_{j}\\ \end{cases} c[i][j]=0c[i1][j1]+1max(c[i][j1],c[i1][j])i=0orj=0i,j>0andxi=yji,j>0andxi!=yj
代码如下:

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

const int Max = 1005;
int c[Max][Max];//下标代表长度

void lcs(string s1, string s2)
{
    //当i或j等于0的时候,初始化
    for(int i=0; i<=s1.size(); i++)
    {
        c[i][0] = 0;
    }
    for(int i=0; i<=s2.size(); i++)
    {
        c[0][i] = 0;
    }
    s1 = " " + s1;//为了与c数组下标配齐,需要加一个头元素
    s2 = " " + s2;
    //动态规划
    for(int i=1; i<=s1.size(); i++)
    {
        for(int j=1; j<=s2.size(); j++)
        {
            if(s1[i]==s2[j]) c[i][j] = c[i-1][j-1]+1;
            else
            {
                c[i][j] = max(c[i][j-1], c[i-1][j]);
            }
        }
    }
}
int main()
{
    int n;
    cin >> n;
    string s1, s2;
    for(int i=0; i<n; i++)
    {
        cin >> s1 >> s2;
        lcs(s1, s2);
        cout << c[s1.size()][s2.size()] << endl;
    }
    return 0;
}
最长递增子序列(LIS)

题目17.3链接Longest Increasing Subsequence
参考博文求LIS的两种方法:DP 与 二分法 ~~
LIS有两种方法,DP(O(N^2))和二分法(O(Nlog(N)),当数据量较大时使用二分法。
解析:可以设置以下的变量

变量含义
L[i]L[i]表示长度为i的递增子序列的末尾元素最小值
l e n g t h i length_{i} lengthi表示前i个元素构成的最长递增子序列的长度

二分搜索法:
思路:我们过程是一直更新L数组,有两种情况

  • A[i]>L[length-1] 则扩展L数组,L[++length] = A[i]。(因为A[i]可以扩展递增子序列的长度,所以加入该序列)
  • A[i]<=L[length-1] 则更新L数组,二分搜索更新L数组中第一个大于等于A[i]的元素为A[i]。(因为L[i]代表的是长度为i的递增子序列的末尾元素(且是所有递增子序列中末尾元素最小的那一个),L[i]中小于A[i]的元素均可以与A[i]组成递增子序列,所以可以使用A[i]更新第一个大于等于它的元素)。

代码如下:

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

const int MAX = 100005;
int n, length;
int A[MAX], L[MAX];

void LIS()
{
    L[0] = A[0];
    length = 1;
    for(int i=1; i<n; i++)
    {
        if(L[length-1]>=A[i])
        {
            int *p = lower_bound(L,L+length,A[i]);
            *p = A[i];
        }
        else
        {
            L[length++] = A[i];
        }
    }
}
int main()
{

    cin >> n;
    for(int i=0; i<n; i++)
    {
        cin >> A[i];
    }
    LIS();
    cout << length << endl;
    return 0;
}
  • 将L[]初始化为INF能够简化代码
//核心代码
int L[MAX]
void solve()
{
	fill(L, L+n, INF);//填充函数
	for(int i=0; i<n; i++)
	{
		*lower_bound(L, L+n, INF) = L[i];
	}
	printf("%d\n", lower_bound(L, L+n, INF) - dp);//计算长度

DP法:

  • 构造状态:L[i]代表以第i个字符结尾的字符串的最大递增子串的长度。
  • 终态:max(L[i]),i>0 && i<=N
  • 状态转化方程:L[i] = max{1 , L[j]+1}   ( j<i && A[j]<A[i] )
  • 初态:L[i] = 1,i>0 && i<=N
    核心代码:
int Max = 1;
for(int i=1; i<=p; i++)
            L[i] = 1;
for(int i=2; i<=p; i++)
{
    for(int j=0; j<i; j++)
    {
        if(x[i]>x[j])
            L[i] = max(L[i], L[j]+1);
    }
    if(Max<L[i]) Max = L[i];
}
cout << Max << endl;
矩阵链乘法

题目11.4链接Matrix Chain Multiplication
解析:可以设置以下变量

变量含义
m[n+1][n+1]m[i][j]表示计算 ( M i M i + 1 . . . M j ) (M_{i}M_{i+1}...M_{j}) MiMi+1...Mj是所需乘法运算的最小次数
p[n+1]用于存储矩阵的行列数,其中 M i 是 p [ i − 1 ] × p [ i ] M_{i}是p[i-1] \times p[i] Mip[i1]×p[i]的矩阵

思路是遍历所有可能的分割情况,得出 ( M i M i + 1 . . . M j ) (M_{i}M_{i+1}...M_{j}) MiMi+1...Mj的最小运算次数,即
m [ i ] [ j ] = { 0 , i = j m i n i ≤ k &lt; j ( m [ i ] [ k ] + m [ k + 1 ] [ j ] + p [ i − 1 ] × p [ k ] × p [ j ] ) , i &lt; j m[i][j] = \begin{cases} 0 &amp; &amp; ,i=j \\ min_{i \leq k &lt;j}(m[i][k]+m[k+1][j]+p[i-1]\times p[k]\times p[j]) &amp; &amp; ,i&lt;j\\ \end{cases} m[i][j]={0minik<j(m[i][k]+m[k+1][j]+p[i1]×p[k]×p[j])i=ji<j

  • 递推法:
    代码如下:
#include <iostream>
using namespace std;

const int Max = 200;
int p[Max];
int m[Max][Max];

int main()
{
     int n;
     cin >> n;
     for(int i=1; i<=n; i++)
     {
         cin >> p[i-1] >> p[i];
     }
     for(int i=1; i<=n; i++)//初始化
        m[i][i] = 0;
     for(int l=2; l<=n; l++)//l代表矩阵链的长度,从2到n
     {
         for(int i=1; i<=n-l+1; i++)//i为矩阵链起始下标
         {
             int j=i+l-1;//j为矩阵链末尾下标
             m[i][j] = (1<<21);//初始为极大的值
             for(int k=i; k<j; k++)//遍历出最优解
             {
                 m[i][j] = min(m[i][j], m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j]);
             }
         }
     }
     cout << m[1][n] << endl;
     return 0;
}
  • 记忆化搜索法
    代码如下:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;

const int MAX = 105;
const int INF = 1<<29;
struct Matrix
{
    int r, c;
};
Matrix M[MAX];
int dp[MAX][MAX], n;

int DP(int i, int j)
{
    int &ans = dp[i][j];
    if(ans!=-1) return ans;
    ans = INF;
    for(int k=i; k<j; k++)
    {
        ans = min(ans, DP(i, k)+DP(k+1, j)+M[i].r*M[k].c*M[j].c);
    }
    return ans;
}
int main()
{
    scanf("%d", &n);
    memset(dp, -1, sizeof(dp));
    for(int i=1; i<=n; i++)
    {
        scanf("%d%d", &M[i].r, &M[i].c);
        dp[i][i] = 0;
    }
    printf("%d\n", DP(1, n));
    return 0;
}
硬币问题

题目17.1链接 Coin Changing Problem
解析:可以设置以下的变量

变量含义
C[i]C[i]表示第i种硬币的面值
T[i][j]T[i][j]表示使用第0至第i种硬币支付j时的最少硬币枚数

可得递归式子:
T[i][j] = min(T[i-1][j], T[i][j-C[i]]+1)
T[i-1][j]代表不使用第i种硬币, T[i][j-C[i]]+1代表使用第i种硬币,去二者最小的即可。
在T矩阵中可知,其值由其正上方的值和其左边的值决定,所以可以使用动态规划,并简化T为一维数组,从小到大一次求值。

变量含义
T[n]T[j]表示支付j元的硬币最少枚数

递归公式可化为:
T[j] = min(T[j], T[j-C[i]]+1)

代码如下:

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

const int Max1 = 50005;
const int Max2 = 30;
const int INF = (1<<29);
int C[Max2], T[Max2][Max1];
int T2[Max1];

int main()
{
    int n, m;
    cin >> n >> m;
    for(int i=1; i<=m; i++)
    {
        cin >> C[i];
    }
    //sort(C+1, C+m+1);//没必要排序
    for(int j=0; j<=n; j++)
    {
        T2[j] = INF;
    }
    T2[0] = 0;
    for(int i=1; i<=m; i++)//动态规划存数组
    {
        for(int j=C[i]; j<=n; j++)
        {
            T2[j] = min(T2[j], T2[j-C[i]]+1);
        }
    }
    cout << T2[n] << endl;
    return 0;
}

/*二维数组
int main()
{
    int n, m;
    cin >> n >> m;
    for(int i=1; i<=m; i++)
    {
        cin >> C[i];
    }
    sort(C+1, C+m+1);
    for(int j=0; j<=n; j++)
    {
        T[0][j] = INF;
    }
    for(int i=0; i<=m; i++)
    {
        T[i][0] = 0;
    }
    for(int i=1; i<=m; i++)
    {
        for(int j=1; j<=n; j++)
        {
            if(j>=C[i])
                T[i][j] = min(T[i-1][j], T[i][j-C[i]]+1);
            else
                T[i][j] = T[i-1][j];
        }
    }
    cout << T[m][n] << endl;
    return 0;
}
*/
0-1背包问题

题目17.2链接 0-1 Knapsack Problem
解析:可以设置以下的变量

变量含义
items[N+1]是一个结构体数组,item[i].v、item[i].w分别记录第i个物品的价值和重量
C[N+1][W+1]C[i][w]表示前i个物品装入容量为w的背包是总价值的最大值

思路:因为每种只有一个物品,所以就是选择放与不放
C [ i ] [ w ] = { 0 , i = 0 o r w = 0 m a x ( C [ i − 1 ] [ w ] , C [ i − 1 ] [ w − i t e m [ i ] . w ] + i t e m [ i ] . v ) , i t e m [ i ] . w &lt; = w C [ i − 1 ] [ w ] , i t e m [ i ] . w &gt; w C[i][w] = \begin{cases} 0 &amp; &amp; ,i=0 or w=0 \\ max(C[i-1][w], C[i-1][w-item[i].w]+item[i].v) &amp; &amp; ,item[i].w&lt;=w\\ C[i-1][w] &amp; &amp; ,item[i].w&gt;w \end{cases} C[i][w]=0max(C[i1][w],C[i1][witem[i].w]+item[i].v)C[i1][w]i=0orw=0item[i].w<=witem[i].w>w
代码如下:

#include <iostream>
using namespace std;

const int Max = 200;
const int W = 10005;
struct Node
{
    int v, w;
};
Node item[Max];
int C[Max][W];

int main()
{
    int N, W;
    cin >> N >> W;
    for(int i=1; i<=N; i++)
    {
        cin >> item[i].v >> item[i].w;
    }
    for(int i=0; i<=N; i++)//初始化
    {
        C[i][0] = 0;
    }
    for(int w=0; w<=W; w++)
    {
        C[0][w] = 0;
    }
    for(int i=1; i<=N; i++)
    {
        for(int w=1; w<=W; w++)
        {
            if(item[i].w<=w)//放的下
            {
                C[i][w] = max(C[i-1][w], C[i-1][w-item[i].w]+item[i].v);
            }
            else
            {
                C[i][w] = C[i-1][w];
            }
        }
    }
    cout << C[N][W] << endl;
    return 0;
}
最大正方形

题目17.4链接Largest Square
解析:可以设置以下的变量

变量含义
dp[H][W]dp[i][j]存储着从(i,j)向左上方扩展可形成的最大正方形的边长。
G[H][W]G[i][j]存储着输入的图(i,j)元素

dp[i][j]的值等于其左上、上方。左侧元素中最小的值加一.
其中初始化时,dp[0][j] = 1-G[0][j],dp[i][0] = 1-G[i][0]。
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])+1.
代码如下:

#include <iostream>
using namespace std;

const int Max = 1500;
int G[Max][Max], dp[Max][Max];
int H, W, LS;

int main()
{
   cin >> H >> W;
   LS = 0;//最大正方形边长
   for(int i=0; i<H; i++)
   {
       for(int j=0; j<W; j++)
       {
           cin >> G[i][j];
       }
   }
   for(int i=0; i<H; i++)//初始化
   {
       dp[i][0] = 1-G[i][0];
       LS = max(dp[i][0], LS);
   }
   for(int j=0; j<W; j++)
   {
       dp[0][j] = 1-G[0][j];
       LS = max(dp[0][j], LS);
   }
//动态规划
   for(int i=1; i<H; i++)
   {
       for(int j=1; j<W; j++)
       {
           if(G[i][j])
               dp[i][j] = 0;
           else
           {
               dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1]))+1;
           }
           LS = max(LS, dp[i][j]);
       }
   }
   cout << LS*LS << endl;
   return 0;
}
最大长方形

题目17.5链接Largest Rectangle
解析:可以设置以下的变量

变量含义
SS是栈,其中的元素为一个结构体(包括元素高度,和矩形最左的边界)

栈+DP
四种情况:rect为结构体的一个元素

  • 栈为空-将rect压入栈
  • 栈顶长方形的高小于rect的高-将rect压入栈
  • 栈顶长方形的高等于rect的高-不做处理
  • 栈顶长方形的高大于rect的高-更新最大长方形的面积

具体解法见代码:

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

const int Max = 1500;
struct Node//结构体,height为元素的高度,pos为包括该元素的矩形的最左边界
{
    int height, pos;
};
int H, W;
int G[Max][Max], T[Max][Max];//G为输入图,T为标记图

int main()
{
    cin >> H >> W;
    for(int i=0; i<H; i++)
    {
        for(int j=0; j<W; j++)
        {
            cin >> G[i][j];
        }
    }
    //标记
    for(int j=0; j<W; j++)
    {
        for(int i=0; i<H; i++)
        {
            if(G[i][j])//有污迹
            {
               T[i][j] = 0;
            }
            else
            {
                if(i==0)//第一行
                    T[i][j] = 1;
                else//计算高度
                    T[i][j] = T[i-1][j]+1;
            }
           
        }
   
    }

    //栈式动态规划
    stack<Node> S;
    int maxv = 0, area = 0;
    for(int i=0; i<H; i++)
    {
        T[i][W] = 0;//用于计算j=W-1时加入新矩形的更新
        for(int j=0; j<=W; j++)
        {
            Node rect;
            rect.height = T[i][j];
            rect.pos = j;
            if(S.empty())
                S.push(rect);
            else
            {
                if(S.top().height<rect.height)
                {
                    S.push(rect);
                }
                else if(S.top().height>rect.height)
                {
                    int target = j;
                    while(!S.empty() && S.top().height>=rect.height)//一直操作到栈中元素height小于rect的height
                    {
                        Node pre = S.top(); S.pop();
                        area = (j-pre.pos)*pre.height;//计算面积
                        maxv = max(maxv, area);
                        target = pre.pos;//更新矩形的最左边界
                    }
                    rect.pos = target;
                    S.push(rect);//更新完面积后入栈
                }
            }
        }
    }
    cout << maxv << endl;
    return 0;
}
背包问题

参考博文背包问题小总结 习题(动态规划01背包(第k优解)完全背包,多重背包)

  • 注意例题2的将问题分成两个子问题的解决方法。
  • 01背包第k个最优解
int i,j,t,k;//k是第k个最优解
for(i=0;i<n;i++) //对每个物品扫描
		for(j=v;j>=vol[i];j--) //对每个状态进行更新 
		{
		
			for(t=1;t<=k;t++)
			{ //把所有可能的解都存起来
				a[t]=f[j][t];//f[j][0]是该序列中最优的
				b[t]=f[j-vol[i]][t]+val[i];//t=0时也是该序列中最优的
			}
			int m,x,y;
			m=x=y=1;
			a[k+1]=b[k+1]=-1;
			//下面的循环相当于求a和b并集,也就是所有的可能解 (求出a和b中前k优解,由m控制,按顺序从优到次)
			while(m<=k && (a[x]!=-1 || b[y]!=-1))
			{
				if(a[x]>b[y])
					f[j][m]=a[x++];
				else 
					f[j][m]=b[y++];	
				if(f[j][m]!=f[j][m-1])
					m++;
			}
		}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值