动态规划经典例题

发布时间 2023-12-04 15:47:41作者: 栎眠尔

由于自己的算法不太好,在学习动态规划时参考了一些别人大佬的博客。下面是我在学习时参考的文章。

矩阵连乘问题

【算法笔记】动态规划:矩阵连乘问题

 理解文章后,我的代码如下:

#include<stdio.h>

void calculate(int m, int n, int rc[], int table[][100], int s[][100]);//求解表中每一个值的具体过程 
void Matrix(int n, int rc[], int table[][100], int s[][100]);//填表 
void output(int x, int y, int s[][100]);//输出加括号的结果 

/*
    参数说明:
    table[][]  Ai...Aj连乘的最少次数 
    rc[] 每个矩阵的行列信息
    s[][]  Ai...Aj连乘的最优解第一层断开位置
    count1  控制行下标的输入
    count2  控制列下标的输入
    rc数组中存储的信息:
    0 —(n-1) A0 —An-1的行信息
    n —(2*n-1)  A0 —An-1的列信息
    
*/

/*
    解题关键:
        1.题目信息:
             给定n个矩阵{A1,A2,...,An},其中Ai与Ai+1是可乘的(i=1,2,...,n-1)    
        2.理解矩阵连乘:
            A是一个p×q矩阵,B是一个q×r矩阵,AB相乘,得到矩阵元素个数为p×r,每个元素由q次乘法得到,
    因此需要乘法次数p×q×r
        3.利用table已知数值
            计算table[i][j]只会使用到它的左下方的矩阵元素数值,因此填table的数据从下至上,从左到右 
*/ 

/*
    测试数据:
    6
    30 35
    35 15
    15 5
    5 10
    10 20
    20 25
    
    结果:
    15125
    ((A1(A2A3))((A4A5)A6)) 
*/ 
int main()
{
    int n;
    scanf("%d", &n);
    int table[n+1][100] = {0};
    int rc[2*n]; 
    int s[n+1][100];
    int count1 = 0, count2 = n;
    for(int i = 0; i < 2*n; i++)
    {
        if(i%2 == 0)
        {
            scanf("%d", &rc[count1]);
            count1++;
        }
        else
        {
            scanf("%d", &rc[count2]);
            count2++;
        }
    }
    Matrix(n, rc, table, s);
    printf("%d", table[1][n]);
    output(1, n, s); 
    
    /*
    调试助手 
    for(int i = 0; i < 2*n; i++)
        printf("%4d", rc[i]);
    printf("\n");
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            printf("%10d", table[i][j]);
        }
        printf("\n");
    }*/
    return 0; 
}

void calculate(int m, int n, int rc[], int table[][100], int s[][100], int N)
{
    table[m][n] = table[m][m] + table[m+1][n] + rc[m-1]*rc[m+N-1]*rc[n-1+N];
    s[m][n] = m;
    for(int i = m+1; i < n; i++)
    {
        int temp = table[m][i] + table[i+1][n] + rc[m-1]*rc[i+N-1]*rc[n-1+N];
        if(temp < table[m][n])
        {
            table[m][n] = temp;    
            s[m][n] = i;
        }
    } 
}

void Matrix(int n, int rc[], int table[][100], int s[][100])
{
    for(int i = n; i > 0; i--)
    {
        for(int j = i; j <= n; j++)//保证填表的右上角部分 
        {
            if(i == j)
                table[i][j] = 0;
            else
                 calculate(i, j, rc, table, s, n);
        }
    }
}

void output(int x, int y, int s[][100])
{
    if(x == y)//递归调用的出口 
        printf("A%d", x);
    else
    {
        printf("(");
        int temp = s[x][y];
        output(x,temp,s);
        output(temp+1,y,s);
        printf(")");
    }
}

最大子段和

#include<stdio.h>

void cal(int n, int arr[], int max[], int s[]);

/*
    参数说明:
    arr[]  存储每个数据
    max[]  从头到i为止最大的子段和 
    s[]  状态,记录到目前为止最大字段和的上一个元素的下标 
*/

/*
    测试数据:
    6
    -2 11 -4 13 -5 -2
    测试结果:
    20
    11 -4 13 
*/ 
int main()
{
    int n;
    scanf("%d", &n);
    int arr[n];
    for(int i = 0; i < n; i++)
        scanf("%d", &arr[i]);
    int max[n]={0};
    int s[n]={0};
    cal(n, arr, max, s);
    int m = max[0];
    int index = 0; 
    for(int i = 1; i < n; i++)
        if(m < max[i])
        {
            m = max[i];
            index = i;
        }
    printf("%d\n", m);
    int record = index;
    while(index != s[index])
    {
        index = s[index];    
    }
    for(int i = index; i <= record; i++)
        printf("%5d", arr[i]); 
    return 0;
}

void cal(int n, int arr[], int max[], int s[])
{
    max[0] = arr[0];
    s[0] = 0;
    for(int i = 1; i < n; i++)
    {
        if(arr[i] < max[i-1]+arr[i])
        {
            max[i] = max[i-1]+arr[i];
            s[i] = i-1;
        }
        else
        {
            max[i] = arr[i];
            s[i] = i;
        }
    }
}

 附加:分治法解法

#include<stdio.h>

int cal(int arr[], int lborder, int rborder, int *lbegin, int *rend);

/*
    参数解释: 
    arr[]  数据数组
    lbegin  当前最大字段和开始下标 
    rend     当前最大字段和结束下标
    lborder  数据开始下标 
    rborder  数据结束下标 

*/
int main()
{
    int n;
    scanf("%d", &n);
    int arr[n];
    for(int i = 0; i < n; i++)
        scanf("%d", &arr[i]);
    int lbegin, rend;
    int max = cal(arr, 0, n-1, &lbegin, &rend);
    printf("%d\n", max);
    for(int i = lbegin; i <= rend; i++)
        printf("%4d", arr[i]);
    return 0;    
}

int cal(int arr[], int lborder, int rborder, int *lbegin, int *rend)
{
    if(lborder == rborder)
    {
        *lbegin = lborder;
        *rend = rborder;
        return arr[lborder];
    }    
    else
    {
        int center = (lborder+rborder)/2;
        int lmax = cal(arr, lborder, center, lbegin, rend);
        //临时左边最大字段和开始结束下标记录 
        int ltempbegin = *lbegin;
        int ltempend = *rend;
        int rmax = cal(arr, center+1, rborder, lbegin, rend);
        //临时右边最大字段和开始结束下标记录
        int rtempbegin = *lbegin;
        int rtempend = *rend;
        int lc = arr[center];
        int rc = arr[center+1];
        int temp = 0;
        //临时中间最大字段和开始下标记录
        int ctempbegin = center; 
        for(int i = center; i >= lborder; i--)
        {
            temp += arr[i];//保证结果是连加结果 
            if(temp > lc)
            {
                lc = temp;
                ctempbegin = i;
            }
        }
        temp = 0; 
        //临时中间最大字段和结束下标记录
        int ctempend = center+1;
        for(int j = center+1; j <= rborder; j++)
        {
            temp += arr[j];//保证结果是连加结果 
            if(temp > rc)
            {
                rc = temp;
                ctempend = j;    
            }    
        }
        int max = lc+rc;
        *lbegin = ctempbegin;
        *rend = ctempend;
        if(lmax > max)
        {
            max = lmax;
            *lbegin = ltempbegin;
            *rend = ltempend;
        }    
        if(rmax > max)
        {
            max = rmax;
            *lbegin = rtempbegin;
            *rend = rtempend;        
        }    
        return max;
    }
            
} 

参考文章:最大子段和详解(N种解法汇总)

最长单调子序列

#include<stdio.h>

void cal(int arr[], int table[], int s[], int n);

/*
整体思路:
    利用i之前的数据,计算从arr[0]到arr[i],必须包括arr[i]的最长递增子序列
    table[] 记录以下标i为最后一个元素的递增子序列,序列的个数 
     s[]  记录table[i]最长递增子序列利用到的上一个元素下标,如果序列数量为1的话,值即设为自己的下标i 
检测样例: 
    8
    5 2 8 6 3 6 5 7
*/

int main()
{
    int n;
    scanf("%d", &n);
    int arr[n];
    for(int i = 0; i < n; i++)
        scanf("%d", &arr[i]);
    int table[n] = {1};
    int s[n] = {0};
    cal(arr, table, s, n);
    //计算递增子段和的最大值 
    int max = table[0];
    int index = 0;
    for(int i = 1; i < n; i++)
    {
        if(table[i] > max)
        {
            index = i;
            max= table[i];
        }
    }
    printf("%d\n", max);
//    printf("index = %d\n", index);
    //根据s数组的内容找出最大递增子段和的每个元素(逆序的) 
    //t中间数组帮助正序输出最大递增子段和的每个元素 
    int t[max];
    int count = 0;    
    for(;;)
    {
//        printf("%d ", arr[index]);
        t[count] = arr[index];
        count++;
        index = s[index];
        if(s[index] == index)
        {
//            printf("%d ", arr[index]);
            t[count] = arr[index];
            break;
        }
    }
    //正序输出每个元素 
    for(int i = max-1; i >= 0; i--)
        printf("%d ", t[i]);
/* 
    printf("\n");
    for(int i = 0; i < n; i++) 
        printf("%4d", i);
    printf("\n");
    for(int i = 0; i < n; i++) 
        printf("%4d", arr[i]);
    printf("\n");
    for(int i = 0; i < n; i++) 
        printf("%4d", table[i]);
    printf("\n");
    for(int i = 0; i < n; i++) 
        printf("%4d", s[i]);
*/
    return 0;
}

void cal(int arr[], int table[], int s[], int n)
{
    for(int i = 1; i < n; i++)
    {
        //默认最长子序列的长为1.即只有自己一个元素 
        int tlen = 1;
        for(int j = 0; j < i; j++)
        {
            if(arr[i] > arr[j] && tlen < table[j]+1)
            {
                tlen = table[j]+1;
                s[i] = j;
            }
        }
        table[i] = tlen;
        if(tlen == 1)
            s[i] = i;
    }
}