algorithm——动态规划

本文深入解析了一系列经典算法问题,包括递归、动态规划、最长上升子序列及背包问题,通过具体实例展示了算法的设计思路与实现过程。

一,简单递归问题

HDU 2044

#include"iostream"
using namespace std;
const int maxn=110;
long long dp[maxn];
int main()
{
    int t,a,b;
    cin>>t;
    while(t--)
    {
        cin>>a>>b;
        dp[0]=1;
        dp[1]=1;
        for(int i=2;i<=b;i++)
            dp[i]=dp[i-1]+dp[i-2];
        cout<<dp[b-a]<<endl;
    }
    return 0;
}

从1到2: 1→2;
从1到3: 1→2→3,1→3;
从1到4: 1→2→3→4,1→3→4,1→2→4;
从1到5: 1→2→3→4→5,1→2→4→5,1→3-→4→5,1→2→3→5,1→3→5;
从1到6:1→2→3→4→5→6,1→2→3→4→6,1→2→3→5→6,1→2→4→5→6,1→3→4→5→6,1→2→4→6,1→3→4→6,1→3→5→6;
第 n 个的路径为 f(n)=f(n-1)+f(n-2)(斐波那契数列)。

注意dp数组定义为long long,不然会WA。

HDU 2045

#include"iostream"
#include"cstring"
using namespace std;
const int maxn=52;
long long dp[maxn];
int main()
{
    int n;
    while(cin>>n)
    {
        memset(dp,0,sizeof(dp));
        dp[1]=3;
        dp[2]=6;
        dp[3]=6;
        for(int i=4;i<=n;i++)
            dp[i]=dp[i-1]+2*dp[i-2];
        cout<<dp[n]<<endl;
    }
    return 0;
}

我们记f[n]表示长度为n的格子涂色共有f[n]种合法涂法。
涂n个格子有两种方法:
方法一:长度为n-1的格子共有f[n-1]种合法涂法,那么我们在可以在他后面加一个格子涂色,前n-1个格子涂色一定合法,说明第1个和第n-1个格子颜色不一样,那么第n个格子的涂色就只能涂一种,
共有f[n-1]种。
方法二:由长度为n-1的格子的不合法涂法来涂n个格子。长度为n-1的不合法涂法一定由n-2的合法涂法而来(在第n-1个涂与第一个格子相同的颜色,有f[n-2]种。那么第n个格子就可以涂与第一个不同的另外两种颜色,所以总共有2f[n-2]种。
所以递推公式为:f[n]=f[n-1]+2
f[n-2];(方法一+方法二)。

HDU 2046

#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=110;
long long dp[maxn];
int main()
{
    int n;
    while(cin>>n)
    {
        memset(dp,0,sizeof(dp));
        dp[1]=1;
        dp[2]=2;
        for(int i=3;i<=n;i++)
        {
            dp[i]=dp[i-1]+dp[i-2];
        }
        cout<<dp[n]<<endl;
    }
    return 0;
}

观察试题可得发现f(1)=1,f(2)=2,f(3)=3,且最后一个骨牌的排列方式有两种:
1.竖排,n格之前就有f(n-1)种排列方式
2.横排,n-1格之前就有f(n-2)种排列方式
得出:f(n)=f(n-1)+f(n-2) (n>=3)

HDU2048

#include"iostream"
#include"algorithm"
#include"cmath"
using namespace std;
const int maxn=50;
long long a[maxn];
long long b[maxn];
void get()
{
    a[1]=0;
    a[2]=1;
    for(int i=3;i<=20;i++)
    {
        a[i]=(i-1)*(a[i-1]+a[i-2]);
    }
}
void num()
{
    b[0]=1;
    b[1]=1;
    for(int i=2;i<=20;i++)
    {
        b[i]=b[i-1]*i;
    }
}
int main()
{
    num();
    get();
    int n,m;
    while(cin>>n)
    {
        while(n--)
        {
            cin>>m;
            printf("%.2lf%%\n",a[m]*100.0/b[m]);
        }
    }

}

错排公式
F[n]=(n-1)(F[n-1]+F[n-2])
n 个不同元素的一个错排可由下述两个步骤完成:
第一步,“错排” 1 号元素(将 1 号元素排在第 2 至第 n 个位置之一),有 n - 1 种方法。
第二步,“错排”其余 n - 1 个元素,按如下顺序进行。视第一步的结果,若1号元素落在第 k 个位置,第二步就先把 k 号元素“错排”好, k 号元素的不同排法将导致两类不同的情况发生:
1、 k 号元素排在第1个位置,留下的 n - 2 个元素在与它们的编号集相等的位置集上“错排”,有 f(n -2) 种方法;
2、 k 号元素不排第 1 个位置,这时可将第 1 个位置“看成”第 k 个位置(也就是说本来准备放到k位置为元素,可以放到1位置中),于是形成(包括 k 号元素在内的) n - 1 个元素的“错排”,有 f(n - 1) 种方法。据加法原理,完成第二步共有 f(n - 2)+f(n - 1) 种方法。

HDU 2049

#include"iostream"
using namespace std;
long long a[21];
void get()
{
    a[1]=0;
    a[2]=1;
    for(int i=3;i<=20;i++)
    {
        a[i]=(i-1)*(a[i-1]+a[i-2]);
    }
}
long long Cn(int n,int m)
{
    long long ans=1;
    if(m>n/2)
    m=n-m;
    for(int i=n;i>n-m;i--)
    ans=ans*i;
    for(int i=m;i>0;i--)
    ans=ans/i;
    return ans;
}
int main()
{
    int n,m,t;
    get();
    while(cin>>t)
    {
        while(t--)
        {
            cin>>n>>m;
            cout<<a[m]*Cn(n,m)<<endl;
        }
    }
    return 0;
}

错排加全排。

HDU 2050

#include"iostream"
using namespace std;
int main()
{
    int n,m;
    while (cin>>n)
    {
        while (n--)
        {
            cin>>m;
            cout<<2*m*m-m+1;
            cout<<endl;
        }
    }
    return 0;    
}

在这里插入图片描述
在这里插入图片描述

折线分平面问题 (该笔记经转载,忘记了原链接)

HDU 2018

#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=110;
long long dp[maxn];
int main()
{
    int n;
    while(cin>>n&&n)
    {
        memset(dp,0,sizeof(dp));
        dp[1]=1;
        dp[2]=2;
        dp[3]=3;
        for(int i=4;i<=n;i++)
        {
            dp[i]=dp[i-1]+dp[i-3];
        }
        cout<<dp[n]<<endl;
    }
    return 0;
}

第1年:1头牛

第2年:2头牛

第3年:3头牛

第4年:4头牛

第5年:6头牛

第6年:9头牛

第7年:13头牛

第8年:19头牛

第9年:28头牛

由此可得:
f(n)=n    n<=4
f(n)=f(n-1) + f(n-3)     n>4

其中,f(n)是当前年的牛数,f(n-1)是前一年的牛,第n年仍在,f(n-3)是前三年那一年的牛,第n年具有生育能力。

HDU 2041

#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=110;
long long dp[maxn];
int main()
{
    int n,t;
    cin>>t;
    while(t--)
    {
        memset(dp,0,sizeof(dp));
        cin>>n;
        dp[1]=1;
        dp[2]=1;
        dp[3]=2;
        for(int i=4;i<=n;i++)
        {
            dp[i]=dp[i-1]+dp[i-2];
        }
        cout<<dp[n]<<endl;
    }
    return 0;
}

依旧是找规律的斐波那契数列。

HDU 2047

#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=110;
long long dp[maxn];
int main()
{
    int n,t;
    while(cin>>n)
    {
        memset(dp,0,sizeof(dp));
        dp[1]=3;
        dp[2]=8;
        for(int i=3;i<=n;i++)
        {
            dp[i]=2*(dp[i-1]+dp[i-2]);
        }
        cout<<dp[n]<<endl;
    }
    return 0;
}

由于长度是n的字符串只有’E’,’O’,’F’组成,并且不能有两个’O’相连,所以我们假设一共有M(n)种排列的可能。而这M(n)中可能又可以分为一下两种:
1)最后一个字符是‘O’:
对于这种情况,很显然第n-1个位置不能是’O’,否则会出现两个’O’相邻的情况,所以此时前面n-1个位置有2M(n-2)种可能。而第n个位置此时是’O’,所以此时共有2乘M(n-2)种可能;
2)最后一个字符不是 ‘O’:
对于这种情况,那么对前面n-1个位置没有特殊的要求,所以对于前面n-1个位置有M(n-1)种可能,而第n个位置有两种可能,所以此时共有2乘M(n-2)种可能。
所以表达式为:M(n)=2
[M(n-1)+M(n-2)]。

二, 简单DP入门

POJ 1163

#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
int a[105][105],dp[105][105],n;
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=i;j++)
        {
            scanf("%d",&a[i][j]);
            dp[i][j]=-1;
        }
    }
    for(int i=1;i<=n;i++)
    {
        dp[n][i]=a[n][i];
    }
    for(int i=n-1;i>=1;i--)
    {
        for(int j=1;j<=i;j++)
        {
           dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+a[i][j];
        }
    }
    cout<<dp[1][1]<<endl;
    return 0;
}

需要用抽象的方法思考问题:把当前的位置(i,j)看成一个状态,然后定义状态(i,j)的指标函数d(i,j)为从格子(i,j)出发时能得到的最大和(包括(i,j)本身的值)。在这个状态定义下,原问题的解为d(1,1).
从格子(i,j)出发有两种决策,往左下走或者往右下走,应选择d(i+1,j),d(i+1,j+1)中较大的那一个,即
d(i,j)=(i,j)+max{d(i+1,j),d(i+1,j+1)}
HDU 2069

#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
int n,dp[110][310];
int m[5]={1,5,10,25,50};
int main()
{
    while(~scanf("%d",&n))
    {
        memset(dp,0,sizeof(dp));
        dp[0][0]=1;
        for(int i=0;i<5;i++)
            for(int j=1;j<=100;j++)
                for(int k=n;k>=m[i];k--)
                    dp[j][k]+=dp[j-1][k-m[i]];
        int ans=0;
        for(int i=0;i<=100;i++)
            ans+=dp[i][n];
        printf("%d\n",ans);
    }
    return 0;
}

状态转移方程 dp[ j ][ k ] = dp[ j-1 ][ k-m[i] ] + dp[ j ] [ k ];

三,最长上升子序列问题

HDU 1257

//二分加贪心
#include"iostream"
#include"cstring"
#include"algorithm"
#include"cmath"
using namespace std;
const int maxn=10001;
int a[maxn],low[maxn];
int main()
{
    int n;
    while(cin>>n)
    {
        memset(a,0,sizeof(a));
        memset(low,0,sizeof(low));
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
        }
        low[1]=a[1];
        int cnt=1;
        for(int i=2;i<=n;i++)
        {
            if(a[i]>low[cnt])
                low[++cnt]=a[i];
            else
            {
                int l=1,r=cnt;
                while(l<=r)
                {
                    int mid=l+r>>1;
                    if(low[mid]<=a[i])
                        l=mid+1;
                    else
                        r=mid-1;
                }
                low[l]=a[i];
            }
        }
        cout<<cnt<<endl;
    }
    return 0;
}
//动态规划求法
/*#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn = 10010, INF = 0x3f3f3f3f;
int a[maxn], f[maxn];
int n,ans = -INF;
int main()
{
    scanf("%d", &n);
    for(int i=1; i<=n; i++) 
    {
        scanf("%d", &a[i]);
        f[i] = 1;
    }
    for(int i=1; i<=n; i++)
        for(int j=1; j<i; j++)
            if(a[j] < a[i])
                f[i] = max(f[i], f[j]+1);
    for(int i=1; i<=n; i++) 
        ans = max(ans, f[i]);
    printf("%d\n", ans);
    return 0;
}*/

新建一个 low 数组,low [ i ]表示长度为i的LIS结尾元素的最小值。对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,我们只需要维护 low 数组,对于每一个a[ i ],如果a[ i ] > low [当前最长的LIS长度],就把 a [ i ]接到当前最长的LIS后面,即low [++当前最长的LIS长度] = a [ i ]。

那么,怎么维护 low 数组呢?
对于每一个a [ i ],如果a [ i ]能接到 LIS 后面,就接上去;否则,就用 a [ i ] 取更新 low 数组。具体方法是,在low数组中找到第一个大于等于a [ i ]的元素low [ j ],用a [ i ]去更新 low [ j ]。如果从头到尾扫一遍 low 数组的话,时间复杂度仍是O(n^2)。我们注意到 low 数组内部一定是单调不降的,所有我们可以二分 low 数组,找出第一个大于等于a[ i ]的元素。二分一次 low 数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。
HDU 1087

#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
int main()
{
    int a[1005],dp[1005],n,i,j,maxn;
    while(scanf("%d",&n)&&n)
    {
        maxn=0;
        memset(dp,0,sizeof(dp));
        for(i=1;i<=n;i++)
            scanf("%d",&a[i]);
        dp[1]=a[1];
        for(i=1;i<=n;i++)
        {
            for(j=1;j<=i;j++)
            {
                if(a[i]>a[j])
                    dp[i]=max(dp[j]+a[i],dp[i]);
            }
            dp[i]=max(dp[i],a[i]);
        }
        maxn=dp[1];
        for(i=1;i<=n;i++)
            maxn=max(dp[i],maxn);
        printf("%d\n",maxn);
    }
    return 0;
}

我们都知道,动态规划的一个特点就是当前解可以由上一个阶段的解推出, 由此,把我们要求的问题简化成一个更小的子问题。子问题具有相同的求解方式,只不过是规模小了而已。最长上升子序列就符合这一特性。我们要求n个数的最长上升子序列,可以求前n-1个数的最长上升子序列,再跟第n个数进行判断。求前n-1个数的最长上升子序列,可以通过求前n-2个数的最长上升子序列。直到求前1个数的最长上升子序列,此时LIS当然为1。

题意是有N个数字构成的序列,求最大递增子段和,即递增子序列和的最大值,思路就是定义dp[i],表示以a[i]结尾的最大递增子段和,双重for循环,每次求出以a[i]结尾的最大递增子段和。
HDU 1159

#include"iostream"
#include"cstring"
#include"algorithm"
using namespace std;
const int maxn=1010;
char s1[maxn],s2[maxn];
int dp[maxn][maxn];
int main()
{
    while(~scanf("%s %s",s1,s2))
    {
        memset(dp,0,sizeof(dp));
        int len1=strlen(s1);
        int len2=strlen(s2);
        for(int i=1;i<=len1;i++)
        {
            for(int j=1;j<=len2;j++)
            {
                if(s1[i-1]==s2[j-1])
                    dp[i][j]=dp[i-1][j-1]+1;
                else
                    dp[i][j]=max(dp[i-1][j],dp[i][j-1]);

            }
        }
        printf("%d\n",dp[len1][len2]);
    }
    return 0;
}

四,各类背包问题

HDU2062

#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=1010;
int n,m,dp[maxn];
pair <int,int> pp[maxn];
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        memset(dp,0,sizeof(dp));
        scanf("%d %d",&n,&m);
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&pp[i].first);
        }
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&pp[i].second);
        }
        for(int i=1;i<=n;i++)
        {
            for(int j=m;j>=pp[i].second;j--)
            {
                dp[j]=max(dp[j],dp[j-pp[i].second]+pp[i].first);
            }
        }
        cout<<dp[m]<<endl;
    }
    return 0;
}

01背包模板
状态转移方程:
定义f[i][j]:前i个物品,背包容量j下的最优解
1)当前背包容量不够(j < w[i]),为前i-1个物品最优解:f[i][j] = f[i-1][j]
2)当前背包容量够,判断选与不选第i个物品
选:f[i][j] = f[i-1][j-w[i]] + v[i]
不选:f[i][j] = f[i-1][j]
状态转移方程为:f[j] = max(f[j], f[j-w[i]] + v[i])。

本题我开了一个pair加一维数组,也可以用上述的二维数组AC;
acwing 3

#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=1005;
int n,dp[maxn];
pair <int,int> pp[1005];
int main()
{
    int n,m;
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d %d",&pp[i].first,&pp[i].second);
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=pp[i].first;j<=m;j++)
        {
            dp[j]=max(dp[j],dp[j-pp[i].first]+pp[i].second);
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

完全背包问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值