线性结构上的动态规划--算法竞赛入门经典笔记

本文为该书的笔记:刘汝佳. 算法竞赛入门经典.第2版[M]. 清华大学出版社, 2014.
动态规划的思想:从复杂的题目背景中抽象出状态表示,然后设计它们之间的转移。

最长上升子序列问题(LTS)

题目链接

状态: d(i) 表示以 i 为结尾的最长上升子序列的长度(A_i 必定为为子序列的最后一个)。 则状态传递函数为:

d(i)=max \left \{ 0,d(j)|j<i,A_j<A_i  \right \}

如果需要打印字典序。
样例:

6
1 6 2 3 7 5
复制代码

输出:

4
1 2 3 5
复制代码

完整程序:

#define LOCAL
#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>
//#include <stdio.h>
#include <cstdlib>
#include<time.h>
#include <ctype.h>
#include <sstream>
#include <assert.h>
#include <math.h>
#include <algorithm>
#include <vector>
#include <set>
#include <map>
#include <stack>
#include <queue>
#define MAXN 92


using namespace std;

int A[101];
int rem[101];
int main()
{
    #ifdef LOCAL
    freopen("data.in","r",stdin);
    freopen("data.out","w",stdout);
    #endif // LOCAL
    int dmax=0,cur=0;
    while(cin >> n&& n){
        memset(d,0,sizeof(d));

        for(int i=1;i<=n;i++){
            cin >> A[i];
        }
        for(int i=1;i<=n;i++){
            d[i]=1;
            for(int j=1;j<i;j++){
                if(A[j]<A[i]){
                    d[i]=max(d[i],d[j]+1);
                }
            }
        }
        for(int i=1;i<=n;i++){
            if(d[i]>dmax){
                cur=i;
                dmax=d[i];
            }
        }
        cout << dmax << endl;
        rem[d[cur]] = A[cur];
        while(d[cur]>1){
            for(int j=1;j<cur;j++){
                if(A[j]<A[cur] && d[j]+1 == d[cur]){
                    cur = j;
                    rem[d[cur]]=A[cur];
                }
            }
        }
        for(int i=1;i<=dmax;i++){
            cout << rem[i] << " ";
        }

  return 0;
}

复制代码

最长公共子序列问题(LCS)

一个数列 {\displaystyle S} ,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则 {\displaystyle S} 称为已知序列的最长公共子序列。

维基百科介绍
样例:

6 7
1 5 2 6 8 7
2 3 5 6 9 8 4
复制代码

输出:

3
复制代码

状态: d(i,j) 表示 A_1 , A_2 ... A_iB_1 , B_2 ... B_j 的 LCS 。
状态转移方程:

d(i,j)= \begin{cases} d(i-1,j-1)+1 & \text{ , } A_i=A_j  \\  max \left \{ d(i-1,j),d(i,j-1) \right \}  & \text{ , } A_i \neq A_j   \end{cases}
#define LOCAL
#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <time.h>
#include <ctype.h>
#include <sstream>
#include <assert.h>
#include <math.h>
#include <algorithm>
#include <vector>
#include <set>
#include <map>
#include <stack>
#include <queue>
#define MAXN 92
using namespace std;

int C;
//int d[10002];
int d[101][101];
int n,m;


int V,W;
int A[101],B[101];
int rem[101];
int dp(int i,int j){
    if(i<=0 || j<=0){
        return 0;
    }
//    cout << i << " " << j << endl;
    int& ans=d[i][j];
    cout << ans << endl;
    if(ans != -1){
        return ans;
    }
    if(A[i] == B[j]){
        return ans=dp(i-1,j-1)+1;
    }
    return ans=max(dp(i-1,j),dp(i,j-1));
}
int main()
{
    #ifdef LOCAL
    freopen("data.in","r",stdin);
    freopen("data.out","w",stdout);
    #endif // LOCAL
    int dmax=0,cur=0;
    while(cin >> n >> m && n && m){
        memset(d,-1,sizeof(d));
        for(int i=1;i<=n;i++){
            cin >> A[i];
        }
        for(int j=1;j<=m;j++){
            cin >> B[j];
        }
        d[0][0]=d[0][1]=d[1][0]=0;
        cout << dp(n,m);

  return 0;
}

复制代码

照明系统设计(UVa11400

样例:

2
54376 266 7 80
20986 679 2 50
复制代码

输出:

1176
复制代码

可以得到结论: A 灯泡换成 B 灯泡,要么全都换,要么全都不换。因为 如果A的一部分换成了B,电源的费用是没有变的,可能省下来的只有当B的灯泡费用小于A的时候的一部分灯泡费用,那么为什么不讲A的所有可以省的灯泡费用都省下来呢?
V_i<V_jK_i-(C_j-C_i)L_i>0 (即 \frac{ K_i}{L_i} +C_i>C_j )时更换。
即如果i可以换成j,j可以换成k,则i则需要换成k。

先把灯泡按照电压从小到大排序,设 s_i 为前 i 种灯泡的总数量。
状态: d[i] 为前 i 种灯泡的最小开销
状态转移方程:

d[i]=min \left \{ d[j]+(s[i]-s[j])*C[i]+K[i] \right \}

其中 (s[i]-s[j])*C[i]+K[i] 为将第 i+1i+2 ... j都换成 j 灯泡之后的开销, (s[i]-s[j])*C[i] 为转换之后这部分的灯泡总费用,

完整程序:

#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring>//操作字符串和数组
#define MAXN 1002

using namespace std;

int n;

int d[MAXN];
int s[MAXN];
struct Lamp{
    int v,k,c,l;
    bool operator < (const Lamp& var){
        return v<var.v;
    }
};
Lamp lamp[MAXN];
int dp(int i){
    int& ans=d[i];
    if(ans != -1){
        return ans;
    }
    ans=s[i]*lamp[i].c+lamp[i].k;
    for(int j=1;j<i;j++){
        ans=min(ans,dp(j)+(s[i]-s[j])*lamp[i].c+lamp[i].k);
    }
    return ans;
}

int main()
{
    #ifdef LOCAL
    freopen("data.in","r",stdin);
    freopen("data.out","w",stdout);
    #endif // LOCAL
    while(cin >> n && n){
        memset(d,-1,sizeof(d));
//        cin >> C;
        for(int i=1;i<=n;i++){
            cin >> lamp[i].v >> lamp[i].k >> lamp[i].c >> lamp[i].l;
        }
        sort(lamp+1,lamp+n+1);
        s[1]=lamp[1].l;
        for(int i=2;i<=n;i++){
            s[i]=s[i-1]+lamp[i].l;
        }
        cout << dp(n) << endl;
  return 0;
}

复制代码

划分成回文串(UVa11584
状态不难想到,困难的是状态转移方程的确定。
状态: d[i] 为字符0~ i划分成的最小回文串个数。
状态转移方程:

d[i]=min{d[j]+1|s[j+1~i]是回文串}
复制代码

使用动态规划判断是s[i~j]是否为回文串: 状态:b[i][j]为bool型,表示i~j为回文串。

b[i][j]=(s[i] == s[j]) && b[i+1][j-1]
复制代码

完整程序:

#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组
#define MAXN 1002


using namespace std;

const int INF = 1 << 30;

int C;

int n, m;

string s;
int b[MAXN][MAXN];
int d[MAXN];
int bp(int i, int j)
{
    int &ans = b[i][j];
    if (ans != -1)
        return ans;
    if (i == j)
        return ans = 1;
    if (i > j)
        return 1;
    if (s[i] == s[j] && bp(i + 1, j - 1))
    {
        return ans = 1;
    }
    else
    {
        return ans = 0;
    }
}
int dp(int i)
{
    int &ans = d[i];
    if (ans != -1)
    {
        return ans;
    }
    if (bp(0, i))
    {
        ans = 1;
    }
    else
    {
        ans = INF;
    }
    for (int j = 0; j < i; j++)
    {
        if (bp(j + 1, i))
        {
            ans = min(ans, dp(j) + 1);
        }
    }
    return ans;
}

int V, W;
int A[101], B[101];
int rem[101];
int main()
{
#ifdef LOCAL
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif // LOCAL
    int N;
    cin >> N;

    getline(cin, s);
    while (N--)
    {
        memset(b, -1, sizeof(b));
        memset(d, -1, sizeof(d));
        d[0] = 1;
        getline(cin, s);
        n = s.length();
        cout << dp(n - 1) << endl;
    }

    return 0;
}
复制代码

颜色的长度(UVa1625

完整程序:

#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组

using namespace std;
const int maxn = 5000 + 5;

const int INF = 1 << 30;

char p[maxn], q[maxn];              //p q为输入的字符序列
int n, m;                           //输入的两个序列的长度
int sp[26], ep[26], sq[26], eq[26]; //两个序列每个字母的起始和结束位置
int d[maxn][maxn];
int c[maxn][maxn];
/**< 有多少颜色已经开始但是尚未结束 */
int color(int i, int j)
{
    int c = 0;
    for (int ii = 0; ii < 26; ii++)
    {
        if ((sp[ii] <= i || sq[ii] <= j) && (ep[ii] > i || eq[ii] > j))
        {
            c++;
        }
    }
    return c;
}

/**<  p序列移走了前i个,q移走了前j个,这i+j个字符的跨度最小和*/
int dp(int i, int j)
{
    int &ans = d[i][j];
    if (ans != -1)
    {
        return ans;
    }
    if (!i && j)
    {
        ans = dp(i, j - 1) + c[i][j - 1];
    }
    else if (i && !j)
    {
        ans = dp(i - 1, j) + c[i - 1][j];
    }
    else
    {
        ans = min(dp(i - 1, j) + c[i - 1][j], dp(i, j - 1) + c[i][j - 1]);
    }
    //    cout <<i<<" "<<j<< " "<<ans <<endl;
    return ans;
}

int main()
{
#ifdef LOCAL
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);

#endif // LOCAL
    int N;
    cin >> N;
    while (N--)
    {
        memset(d, -1, sizeof(d));
        cin >> p + 1;
        cin >> q + 1;
        n = strlen(p + 1);
        m = strlen(q + 1);
        for (int i = 1; i <= n; i++)
        {
            p[i] -= 'A';
        }
        for (int i = 1; i <= m; i++)
        {
            q[i] -= 'A';
        }
        for (int i = 0; i < 26; i++)
        {
            sp[i] = sq[i] = INF;
            ep[i] = eq[i] = 0;
        }
        for (int i = 1; i <= n; i++)
        {
            sp[p[i]] = min(sp[p[i]], i);
            ep[p[i]] = i;
        }
        for (int i = 1; i <= m; i++)
        {
            sq[q[i]] = min(sq[q[i]], i);
            eq[q[i]] = i;
        }
        d[0][0] = 0;
        for (int i = 0; i <= n; i++)
            for (int j = 0; j <= m; j++)
            {
                c[i][j] = color(i, j);
            }
        cout << dp(n, m) << endl;
    }

    return 0;
}
复制代码

最优矩阵链乘

假设第 i 个矩阵 A_ip_{i-1} \times p_i
状态 f(i,j) 表示:“把 A_iA_{i+1} , ... , A_j乘起来最少需要多少次乘法”。 状态转移方程:

f(i,j)=min \left \{ f(i,k)+f(k+1,j)+p_{i-1} p_k p_j \right \}

样例:

3
2 3 4 5
复制代码

结果:

64
复制代码

完整程序:
记忆化搜索:

#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组

using namespace std;
const int maxn = 100 + 5;

const int INF = 1 << 30;
int p[maxn];
int d[maxn][maxn];
int n;
int dp(int i, int j)
{
    int &ans = d[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) + p[i - 1] * p[k] * p[j]);
    }
    return ans;
}
int main()
{
#ifdef LOCAL
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif // LOCAL
    while (cin >> n && n)
    {
        memset(d, -1, sizeof(d));
        for (int i = 0; i <= n; i++)
        {
            cin >> p[i];
            d[i][i] = 0;
        }
        cout << dp(1, n) << endl;
    }
    return 0;
}
复制代码

递推法:

#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组

using namespace std;
const int maxn = 100 + 5;

const int INF = 1 << 30;
int p[maxn];
int d[maxn][maxn];
int n;
int main()
{
#ifdef LOCAL
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif // LOCAL
    while (cin >> n && n)
    {
        memset(d, -1, sizeof(d));
        for (int i = 0; i <= n; i++)
        {
            cin >> p[i];
            d[i][i] = 0;
        }
        for (int delta = 1; delta < n; delta++)
        {
            for (int i = 1; i + delta <= n; i++)
            {
                d[i][i + delta] = INF;
                for (int k = i; k <= i + delta; k++)
                {
                    d[i][i + delta] = min(d[i][i + delta], d[i][k] + d[k + 1][i + delta] + p[i - 1] * p[k] * p[i + delta]);
                }
            }
        }
        cout << d[1][n] << endl;
    }
    return 0;
}
复制代码

最优三角剖分

对于一个n个顶点的凸多边形,有很多种方法可以对它进行三角剖分(triangulation),即用n-3条互不相交的对角线把凸多边形分成n-2个三角形。为每个三角形规定一个权函数w(i, j,k)(如三角形的周长或3个顶点的权和),求让所有三角形权和最大的方案。

最优三角形剖分与最优矩阵链乘的不同:链乘表达式反映了决策过程,而剖分不反映决策过程,即在链乘问题里面对于某一解决策的过程是确定的,而在三角形剖分里面“第一刀”可以是这一个解里面的任何一条对角线。
如果允许随意切割,则半成品多边形的各个顶点可以在原多边形中任意选取的,很难简洁定义成状态。所以有必要把决策的顺序规范化,使得在规范的决策顺序下,任意状态都可以用区间表示。
定义 d(i,j) 为子多边形 ii+1 , ... , j 的最优值,则边 i-j在最优解中一定对应一个三角形 i-j-k ( i<k<j )。 状态转移方程:

d(i,j)=max \left \{ d(i,k)+d(k,j)+w(i,j,k)|i<k<j \right \}

原问题的解为 d(0,n-1)

切木棍(UVa10003

状态: d(i,j) 为切割 i ~j$ 部分的最小费用。 状态转移方程:

d(i,j)=min \left \{ d(i,k)+d(k,j)+a_j-a_i|i<k<j \right \}

完整程序:

#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组

using namespace std;
const int maxn = 50 + 3;

const int INF = 1 << 30;
int n;
int L;

int a[maxn];
int d[maxn][maxn];
int dp(int i,int j){
    if(j==i+1){
        return 0;
    }
    int& ans=d[i][j];
    if(ans != -1){
        return ans;
    }
    ans=INF;
    for(int k=i+1;k<j;k++){
        ans=min(ans,dp(i,k)+dp(k,j)+a[j]-a[i]);
    }
    return ans;
}
int main()
{
#ifdef LOCAL
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif // LOCAL
    while(cin >> L && L){
        memset(d,-1,sizeof(d));
        memset(a,0,sizeof(a));
        cin >> n;
        for(int i=1;i<=n;i++){
            cin >> a[i];
        }
        a[0]=0;
        a[n+1]=L;
        cout <<"The minimum cutting is "<< dp(0,n+1)<< "." << endl;
    }
    return 0;
}
复制代码

括号序列(UVa1626):

定义如下正规括号序列(字符串):

  • 空序列是正规括号序列。
  • 如果S是正规括号序列,那么(S)和[S]也是正规括号序列。
  • 如果A和B都是正规括号序列,那么AB也是正规括号序列。

例如,下面的字符串都是正规括号序列:(),[],(()),([]),()[],()[()],而如下字符串则不是正规括号序列:(,[,],)(,([()。
输入一个长度不超过100的,由“(”、“)”、“[”、“]”构成的序列,添加尽量少的括号,得到一个规则序列。如有多解,输出任意一个序列即可。

完整程序:

#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组
#include <string>

using namespace std;
const int maxn = 100 + 3;

const int INF = 1 << 30;
int n;
string S;
int d[maxn][maxn];
int match(char a, char b)
{
    int c = 0;
    if (a == '(' && b == ')')
    {
        c = 1;
    }
    else if (a == '[' && b == ']')
    {
        c = 1;
    }
    return c;
}
int dp(int i, int j)
{

    int &ans = d[i][j];
    if (ans != -1)
    {
        return ans;
    }
    if (i > j)
        return ans = 0;
    if (i == j)
    {
        return ans = 1;
    }
    ans = INF;
    if (match(S[i], S[j]))
    {
        ans = dp(i + 1, j - 1);
    }
    for (int k = i; k < j; k++)
    {
        ans = min(ans, dp(i, k) + dp(k + 1, j));
    }
    return ans;
}
void print(int i, int j)
{
    if (i > j)
        return;
    if (i == j)
    {
        if (S[i] == '(' || S[i] == ')')
        {
            printf("()");
        }
        else
        {
            printf("[]");
        }
        return;
    }
    int ans = d[i][j];
    if (match(S[i], S[j]) && ans == d[i + 1][j - 1])
    {
        printf("%c", S[i]);
        print(i + 1, j - 1);
        printf("%c", S[j]);
        return;
    }
    for (int k = i; k < j; k++)
    {
        if (ans == d[i][k] + d[k + 1][j])
        {
            print(i, k);
            print(k + 1, j);
            return;
        }
    }
}
int main()
{
#ifdef LOCAL
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif // LOCAL
    int N;
    cin >> N;
    getline(cin, S);
    while (N--)
    {
        memset(d, -1, sizeof(d));
        getline(cin, S);
        getline(cin, S);
        int n = S.length();
        dp(0, n - 1);
        print(0, n - 1);
        cout << endl;
        cout << endl;
    }
    return 0;
}
复制代码

最大面积最小的三角剖分(UVa1331

这实际是一个最优三角形剖分问题。
状态:设d(i,j)为子多边形 ii+1 , ... , j 的最优值(最大面积最小的三角形的最大面积),则边 i-j 在最优解中一定对应一个三角形 i-j-k ( i<k<j )。 状态转移方程:

d(i,j)=min \left \{ max \left \{ d(i,k),d(k,j),w(i,j,k)\right \}|i<k<j \right \}

原问题的解为 d(0,n-1) 。 完整程序:

#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组
#include <string>
#include <math.h>

using namespace std;
const int maxn = 50 + 2;
const int INF = 1 << 30;
int n;
double point[maxn][2];
double d[maxn][maxn];
int vis[maxn][maxn];
//求三角形的面积
double w(int a1, int b1, int c1)
{
    //求三条边长
    double a = sqrt(pow(point[a1][0] - point[b1][0], 2.0) + pow(point[a1][1] - point[b1][1], 2.0));
    double b = sqrt(pow(point[c1][0] - point[b1][0], 2.0) + pow(point[c1][1] - point[b1][1], 2.0));
    double c = sqrt(pow(point[a1][0] - point[c1][0], 2.0) + pow(point[a1][1] - point[c1][1], 2.0));
    double s = (a + b + c) / 2;
    double A = sqrt(s * (s - a) * (s - b) * (s - c));
    return A;
}
double dp(int i, int j)
{
    double area;
    double &ans = d[i][j];
    if (vis[i][j])
    {
        return ans;
    }
    if (j == i + 2)
    {
        vis[i][j] = 1;
        return ans = w(i, i + 1, j);
    }
    if (j == i + 1)
    {
        vis[i][j] = 1;
        return ans = 0;
    }
    ans = INF * 1.0;
    for (int k = i + 1; k <= j - 1; k++)
    {
        area = w(i, j, k);
        area = max(area, dp(i, k));
        area = max(area, dp(k, j));
        ans = min(ans, area);
    }
    vis[i][j] = 1;
    return ans;
}
int main()
{
#ifdef LOCAL
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif // LOCAL
    int N;
    cin >> N;
    while (N--)
    {
        memset(vis, 0, sizeof(vis));
        cin >> n;
        //输入点的值
        for (int i = 0; i < n; i++)
        {
            cin >> point[i][0] >> point[i][1];
        }
        //状态(0,n-1)的最优解
        cout << dp(0, n - 1) << endl;
    }
    return 0;
}
复制代码

转载于:https://juejin.im/post/5a6f1a386fb9a01cb1396729

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值