第三章总结

算法第三章总结

动态规划算法通常用于求解具有某种最有性质的问题。
适用动态规划策略解决的问题具有以下3个性质:

1.最优化原理,也称最优子结构:如果问题的最优解包含的子问题的解也是最优的,就称该问题具有最优子结构,既满足最优化原理。
2.无后向性,即某阶段一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关,这种特性被称为无后向性。
3.重叠子问题;即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用。对有分解过程的问题还表现在;自顶向下分解问题时,每次产生的子问题并不总是新问题,有些子问题会反复出现多次。

动态规划算法的一般求解步骤如下:
1.分段:把所求的最优化问题分成若干个阶段,即将原问题分解为若干个相互重叠的子问题,找出最优解的性质,并刻画其结构特性。
2.分析:将问题各个阶段所处不同的状态表示出来,确定各个阶段状态之间的递推关系(即动态规划函数的递推式),并确立初始条件。分析归纳出各个阶段状态之间的转移关系是应用动态规划的关键。
3.求解:利用递推式自底向上计算,求解最优值。递推计算最优值是动态规划算法的实施过程。
4.构造最优解:根据计算最优值时得到的信息构造最优解。构造最优解就是具有求出最优决策序列。

1.数塔问题
数塔问题与之前的贪心法要求相似,不妨先用贪心法尝试对该问题进行求解。易发现这个问题用贪心法不能保证找到真正的最大和。故放弃贪心法求解本题。
从数塔问题的特点看,易发现解决问题的阶段划分应该是自下而上逐层决策。
以下方法感觉AC更快一些

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>

using namespace std;
const int N = 110;
int n;
int f[N][N];
int *maxsum;

int main(){
    scanf("%d",&n);
    for(int i = 1;i <= n;++i){
        for(int j = 1;j <= i;++j){
            scanf("%d",&f[i][j]);
        }
    }
    maxsum = f[n];
    for(int i = n - 1;i > 0;--i){
        for(int j = 1;j <= i;++j){
            maxsum[j] = max(maxsum[j],maxsum[j + 1]) + f[i][j];
        }
    }
    cout << maxsum[1] << endl;
    return 0;
}

2.矩阵连乘问题
pxq阶与qxr阶的两个矩阵相乘时,共需pxqxr次乘法,且多个矩阵进行相乘运算时满足结合律。由此可见,不同矩阵相乘运算,虽然运算结果相同,但所做的乘法次数差距很大。为了找到不同组合方式下矩阵相乘的最少乘法次数。如果用枚举法,当n很小时,还可胜任,但是当n较大时,算法的复杂程度就非常大了。
所以,该问题不能分解为独立的子问题,且不容易枚举所有的可能解,只能用动态规划设计算法。
首先从最小的子问题开始求解此问题,即2个矩阵相乘的情况,然后尝试3个矩阵相乘的情况,即尝试所有2个矩阵相乘后结合第三个矩阵的方式,从中找到乘法运算最少的结合方式…最后得到n个矩阵相乘所用的最少的乘法次数及结合方式。
dp方程:f[i][j] = min(f[i][k],f[k + 1][j]) (i < j)
f[i][j] = 0 (i = j)
用一个数组存储矩阵相乘的运算步骤即最小值k

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 55;

int cnt[N][N];
int m[N],f[N][N];
int n;

int course(int i,int j){
    if(cnt[i][j] >= 0)  return cnt[i][j];
    if(i == j)  return 0;
    if(i == j - 1){
        f[i][i + 1] = i;
        cnt[i][j] = m[i] * m[i + 1] * m[i + 2];
        return cnt[i][j];
    }
    int u,t;
    u = course(i,i) + course(i + 1,j) + m[i] * m[i + 1] * m[j + 1];
    f[i][j] = i;
    for(int k = i + 1;k < j;++k){
        t = course(i,k) + course(k + 1,j) + m[i] * m[k + 1] * m[j + 1];
        if(t < u){
            u = t;
            f[i][j] = k;
        }
    }
    cnt[i][j] = u;
    return u;
}

int main(){
    int t,i;
    while(scanf("%d",&n) && n != 0){
        for(int i = 1;i <= n;++i){
            scanf("%d%d",&m[i],&t);
        }
        m[i] = t;
        for(int i = 1;i <= n;++i){
            for(int j = 1;j <= n;++j){
                cnt[i][j] = -1;
            }
        }
        cout << course(1,n) << endl;
    }
    return 0;
}

3.最长公共子序列问题
状态表示f[i][j] 集合:所有在第1个序列的前i个字母,和在第二个序列的前j个字母中出现的子序列,属性Max
状态计算:f[i - 1][j - 1] f[i - 1][j] f[i][j - 1] f[i - 1][j - 1] + 1
f[i - 1][j - 1]包含在f[i - 1][j] 和f[i][j - 1]中,f[i - 1][j]包含01 f[i][j - 1]包含10
这四种可能会重叠,但是求Max无关紧要

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1010;
char a[N],b[N];
int f[N][N];

int main(){
    while(scanf("%s %s",a + 1,b + 1)){
        int n = strlen(a + 1),m = strlen(b + 1);
        for(int i = 1;i <= n;++i){
            for(int j = 1;j <= m;++j){
                f[i][j] = max(f[i - 1][j],f[i][j - 1]);
                if(a[i] == b[j])    f[i][j] = max(f[i][j],f[i - 1][j - 1] + 1);
            }
        }
        printf("%d\n",f[n][m]);
    }
    return 0;
}

4.最长上升子序列问题
状态表示: 集合:以第i个数为结尾的上升子序列,属性Max
状态计算:集合的划分:以第i - 1个数为结尾进行划分
f[i] = max(f[j] + 1)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 5010;
int n,res;
int a[N];
int f[N];

int main(){
    scanf("%d",&n);
    for(int i = 1;i <= n;++i)   scanf("%d",&a[i]);
    for(int i = 1;i <= n;++i){
        f[i] = 1;
        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)   res = max(res,f[i]);
    printf("%d\n",res);
    return 0;
}

5.陪审团问题
DP问题找最优解
状态表示: 集合:所有从前i个人中选择j个人,且差值是k的所有方案的集合,属性:Max
状态计算:相当于一个01背包问题
1.所有不选择第i个人的方案f[i - 1,j,k]
2.所有选择第i个人的方案f[i - 1,j - 1,k - (pi - di)] + pi + di
不合法情况:1.判断差值是否在[-400,400],不在则为不合法
2.选择0个人则没有任何意义

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 210,M = 810,base = 400;
int n,m;
int p[N],d[N];
int f[N][21][M];
int ans[N];

int main(){
    int T = 1;
    while(scanf("%d%d",&n,&m),n || m){
        for(int i = 1;i <= n;++i)    scanf("%d%d",&p[i],&d[i]);
        memset(f,-0x3f,sizeof f);
        f[0][0][base] = 0;
        for(int i = 1;i <= n;++i){
            for(int j = 0;j <= m;++j){
                for(int k = 0;k < M;++k){
                    f[i][j][k] = f[i - 1][j][k];
                    int t = k - (p[i] - d[i]);
                    if(t < 0 || t >= M) continue;
                    if(j < 1)   continue;
                    f[i][j][k] = max(f[i][j][k],f[i - 1][j - 1][t] + p[i] + d[i]);
                }
            }
        }
        int v = 0;
        while(f[n][m][base - v] < 0 && f[n][m][base + v] < 0)   v++;
        if(f[n][m][base - v] > f[n][m][base + v])   v = base - v;
        else    v = base + v;
        int cnt = 0;
        int i = n,j = m,k = v;
        while(j){
            if(f[i][j][k] == f[i - 1][j][k])    i--;
            else{
                ans[cnt++] = i;
                k -= (p[i] - d[i]);
                i--,j--;
            }
        }
        int sd = 0,sp = 0;
        for(int i = 0;i < cnt;++i){
            sp += p[ans[i]];
            sd += d[ans[i]];
        }
        printf("Jury #%d\n",T++);
        printf("Best jury has value %d for prosecution and value %d for defence:\n",sp,sd);
        sort(ans,ans + cnt);
        for(int i = 0;i < cnt;++i)  printf(" %d",ans[i]);
        puts("\n");
    }
    return 0;
}

实战训练

1.最少硬币问题
有点类似多重背包问题,从前i个物品中拿,只是该题限制的物品量
状态表示:集合:f[k]:总数为需要的钱币的数量,属性:Min
状态计算:f[k] = min(f[k],f[k - T[i]] + 1)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 2010;
int n,m;
int T[11],Num[11];
int f[N];

int main(){
    scanf("%d",&n);
    for(int i = 1;i <= n;++i)   scanf("%d%d",&T[i],&Num[i]);
    scanf("%d",&m);
    for(int i = 1;i <= m;++i)   f[i] = 0x3f3f3f3f;
    for(int i = 1;i <= n;++i){
        for(int j = 1;j <= m;++j){
            for(int k = m;k >= T[i];--k){
                f[k] = min(f[k],f[k - T[i]] + 1);
            }
        }
    }
    if(f[m] >= m)    printf("-1\n");
    else    printf("%d\n",f[m]);
}

2.编辑距离问题
状态表示:所有从a[1n]变为b[1m]的方案,属性:Min
状态计算:1.删操作:f[i - 1,j] + 1;
2.增操作:f[i,j - 1] + 1
3.改操作:1.当两个字符串最后一位相等时,f[i - 1,j - 1]
2.当两个字符串最后一位不相等时,f[i - 1,j - 1] + 1;

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;
int f[N][N];
char a[N],b[N];

int main(){
    scanf("%s%s",a + 1,b + 1);
    int n = strlen(a + 1),m = strlen(b + 1);
    for(int i = 0;i <= m;++i)   f[0][i] = i;
    for(int i = 0;i <= n;++i)   f[i][0] = i;
    for(int i = 1;i <= n;++i){
        for(int j = 1;j <= m;++j){
            f[i][j] = min(f[i - 1][j] + 1,f[i][j - 1] + 1);
            if(a[i] == b[j])    f[i][j] = min(f[i][j],f[i - 1][j - 1]);
            else    f[i][j] = min(f[i][j],f[i - 1][j - 1] + 1);
        }
    }
    printf("%d\n",f[n][m]);
    return 0;
}

3.石子合并问题

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 310;
int n;
int a[N],s[N];
int f[N][N];

int main(){
    scanf("%d",&n);
    for(int i = 1;i <= n;++i){
        scanf("%d",&a[i]);
        s[i] += s[i - 1] + a[i];
    }
    for(int len = 2;len <= n;++len){
        for(int i = 1;i + len - 1 <= n;++i){
            int j = i + len - 1;
            f[i][j] = 1e8;
            for(int k = i;k <= j - 1;++k){
                f[i][j] = min(f[i][j],f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
            }
        }
    }
    cout << f[1][n] << endl;
    return 0;
}

4.最小m段和问题
用f[i][j]存储长度为i,分j段后其子序列和的最大值的最小值,那么它由两部分构成:
当j=1时,f[i][1]表示的是长为i的整个序列的和
子序列的和的最大值:max(f[k][j-1],f[i][1]-f[k][1]))
当j>1时,f[i][j] = min(max…)
这当中k表示的是分段的最后一段子序列的开始下标,所以f[k][j-1]是前面j-1段子序列和的最大值的最小值,
f[i][1] - f[k][1]是最后一段子序列的和
所以取这两段中的最大值,在k值变化过程中取得到的最小值

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 110;
int n,m;
int f[N][N];
int a[N];

int main(){
    int res;
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;++i)   scanf("%d",&a[i]);
    for(int i = 1;i <= n;++i)   f[i][1] = f[i - 1][1] + a[i];
    for(int i = 1;i <= n;++i){
        for(int j = 2;j <= m;++j){
            int minn = 1e5;
            for(int k = 1;k <= i;++k){
                res = max(f[k][j - 1],f[i][1] - f[k][1]);
                minn = min(minn,res);
            }
            f[i][j] = minn;
        }
    }
    printf("%d\n",f[n][m]);
    return 0;
}

6.最大k乘积问题
最优解问题,动态规划解决
把前i个数字分割成j段,则将前k个数字分成j-1段,将最后i-k个数字作为一段,那么两段的乘积作为前i个数字分割成j段的乘积,从中选取最大值作为f[i][j]的值
类似于上面最小m段和问题
状态转移方程:f[i][j] = max(f[i][j],f[i - 1][m - 1] * cs(m,j));

#include <iostream>
#include <string>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 20;
string s;
LL f[N][N];
LL n,k;

LL cs(int ks,int js){
    LL sum = 0,t = 1;
    for(int i = js;i >= ks;--i){
        sum += (s[i] - '0') * t;
        t *= 10;
    }
    return sum;
}

int main(){
    scanf("%lld%lld",&n,&k);
    cin >> s;
    for(int i = 0;i < n;++i)    f[0][i] = cs(0,i);
    for(int i = 1;i <= k;++i){
        for(int j = 1;j <= n;++j){
            for(int m = j;m >= i;--m){
                f[i][j] = max(f[i][j],f[i - 1][m - 1] * cs(m,j));
            }   
        }
    }
    printf("%d\n",f[k][n - 1]);
    return 0;
}

7.最小费用购物问题、
最多五种商品五维f[n1][n2][n3][n4][n5]表示五种商品的购买数量,属性min
初始化f[0][0][0][0][0] = 0
状态转移方程:f[i][j][k][l][p] = min(f[i][j][k][l][p],f[i - pi][j - pi][k - pi][l - pi][p - pi]) (pi为第i种优惠)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 6;
int n,m;
int s[10][N],sp[10];  //每个优惠中每个商品的数量
int g[N][4];  //编号和单价
int f[N][N][N][N][N];
int st[10],si[10][10];  //优惠商品的价格,编号

int main(){
    scanf("%d",&n);
    for(int i = 1;i <= n;++i)   scanf("%d%d%d",&g[i][1],&g[i][3],&g[i][2]);
    scanf("%d",&m);
    for(int i = 1;i <= m;++i){
        scanf("%d",&st[i]);
        for(int j  =1;j <= st[i];++j){
            scanf("%d",&si[i][j]);
            scanf("%d",&s[i][si[i][j]]);
        }
        scanf("%d",&sp[i]);
    }
    memset(f,0,sizeof f);
    for(int i = 0;i <= g[1][3];++i){  //第几种 数量
        for(int j = 0;j <= g[2][3];++j){
            for(int k = 0;k <= g[3][3];++k){
                for(int l = 0;l <= g[4][3];++l){
                    for(int p = 0;p <= g[5][3];++p){  //单价
                        int minn = i * g[1][2] + j * g[2][2] + k * g[3][2] + l * g[4][2] + p * g[5][2];
                        for(int q = 1;q <= m;++q){
                            if(i - s[q][g[1][1]] < 0 || j - s[q][g[2][1]] < 0 || k - s[3][g[3][1]] < 0 || l - s[q][g[4][1]] < 0 || p - s[q][g[5][1]] < 0)   continue;
                            int t = f[i - s[q][g[1][1]]][j - s[q][g[2][1]]][k - s[q][g[3][1]]][l - s[q][g[4][1]]][p - s[q][g[5][1]]] + sp[q];
                            minn = min(minn,t);
                        }
                        f[i][j][k][l][p] = minn;
                    }
                }
            }
        }
    }
    printf("%d\n",f[g[1][3]][g[2][3]][g[3][3]][g[4][3]][g[5][3]]);
    return 0;
}

8.最优时间表问题
f[i]表示前i所有时间都维修了,对第i个时间做选择,属性min
状态转移方程:用vector存储a
minn = min(f[a[i][j]]) f[i] = minn;

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100;
int n,k,s,t;
int f[N];

int main(){
    scanf("%d%d",&n,&k);
    vector<int> *a = new vector<int>[n + 2]();
    while(k--){
        scanf("%d%d",&s,&t);
        a[s].push_back(s + t);
    }
    f[n + 1] = n;
    for(int i = n;i >= 1;--i){
        if(a[i].empty())    f[i] = f[i + 1] - 1;
        else{
            int minn = 1e8;
            for(int j = 0;j < (int)a[i].size();++j) minn = min(minn,f[a[i][j]]);
            f[i] = minn;
        }
    }
    printf("%d\n",f[1]);
    return 0;
}

9.矩阵嵌套问题
f[i]表示包含前i个矩阵的方案数
状态转移方程:f[i] = max(1,f[j] + 1)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 110;
int n;
int a[N],b[N];
int st[N];
int g[N][N];
int res;

int f(int n){
    if(st[n] != -1) return st[n];
    int ans = 1;
    for(int i = 0;i < n;++i){
        if(g[n][i] == 1)    ans = max(ans,f(i) + 1);
    }
    st[n] = ans;
    return ans;
}

int main(){
    int t,res;
    scanf("%d",&t);
    while(t--){
        res = -1000;
        memset(st,-1,sizeof st);
        memset(g,0,sizeof g);
        scanf("%d",&n);
        for(int i = 0;i < n;++i){
            scanf("%d%d",&a[i],&b[i]);
            if(a[i] > b[i]) swap(a[i],b[i]);
        }
        for(int i = 0;i < n;++i){
            for(int j = 0;j < n;++j){
                if(a[i] > a[j] && b[i] > b[j])  g[i][j] = 1;
            }
        }
        for(int i = 0;i < n;++i)    res = max(res,f(i));
        printf("%d\n",res);
    }
    return 0;
}

10.导弹拦截问题
类似于最长不升子序列问题

#include <iostream>
#include <cstring>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n,m;
int a[N];
int f[N];

int main(){
    scanf("%d",&n);
    for(int i = 0;i < n;++i){
        int res = 0;
        scanf("%d",&m);
        for(int i = 0;i < m;++i)    scanf("%d",&a[i]);
        for(int i = 0;i < m;++i){
            f[i] = 1;
            for(int j = 0;j < i;++j){
                if(a[i] <= a[j])    f[i] = max(f[i],f[j] + 1);
            }
            res = max(res,f[i]);
        }
        printf("%d\n",res);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值