AcWing算法学习之动态规划(基础)

news2024/11/18 9:47:51

背包问题

01背包问题

在这里插入图片描述
思路:
01背包问题,表示每个物品要么放,要么不放。从集合的角度分析DP问题,状态表示为:选择前i个物品,总体积小于等于j的选法的集合,属性f[i][j]表示价值的最大值。状态计算,因为每个状态可以表示为选择当前的物品,或者不选当前的物品,二者价值取最大值即可,即状态转移方程为:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v i ] + w i ) , j ≥ v i f[i][j] = max(f[i - 1][j], f[i - 1][j - v_i]+w_i), j \ge v_i f[i][j]=max(f[i1][j],f[i1][jvi]+wi),jvi
朴素代码:

#include<iostream>
using namespace std;

const int N = 1005;
int n, m, v[N], w[N], f[N][N];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= m; j++)
        {
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) 
            	f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << f[n][m];
    return 0;
}

一维优化

f[i][j] = f[i - 1][j];
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);

变化为
f[j] = f[j] // 省略
f[j] = max(f[j], f[j - v[i]] + w[i])
// 由于f[i][j]需要用到第i - 1层的结果,j-v[i]严格小于j,所以用j-v[i]更新j时,用的是第i层的结果,因此j需要逆序一下,

优化代码:

#include<iostream>
using namespace std;

const int N = 1010;
int v[N], w[N];
int f[N];

int main()
{
    int n, V;
    cin >> n >> V;
    
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i++)
    {
        for (int j = V; j >= v[i]; j--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    }
    
    cout << f[V] << endl;
    
    return 0;
}

完全背包问题

在这里插入图片描述
思路:
完全背包问题,每一个物品可以选任意数量个,从集合的角度分析DP问题,状态表示为:选择前i个物品,总体积小于等于j的选法的集合,属性f[i][j]表示价值的最大值。状态计算,因为每个状态可以表示为选0个,选1个…选k个,即朴素状态转移方程为:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − k × v i ] + k × w i ) , j ≥ k × v i f[i][j] = max(f[i - 1][j], f[i - 1][j - k ×v_i]+k × w_i), j \ge k ×v_i f[i][j]=max(f[i1][j],f[i1][jk×vi]+k×wi),jk×vi
f[i - 1][j]k = 0时候的特殊情况
朴素代码:

#include<iostream>
using namespace std;

const int N = 1010;
int v[N], w[N];
int f[N][N];

int main()
{
    int n, m;
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= m; j++)
        {
            // f[i][j] = f[i - 1][j];
            for (int k = 0; k * v[i] <= m; k++)
            {
                if (j >= k * v[i]) f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
            }
        }
    
    cout << f[n][m] << endl;
    
    return 0;
}

由于是三重循环,时间复杂度为 O ( n 3 ) O(n^3) O(n3),会超时,下面进行优化

f[i][j] = max(f[i - 1][j], f[i - 1][j - vi] + wi, f[i - 1][j - 2vi] + 2wi,...f[i - 1][j - kvi] + kwi)
f[i][j - vi] = max(        f[i - 1][j - vi] , f[i - 1][j - 2vi] + wi,...f[i - 1][j - kvi] + (k - 1)wi)

状态转移方程为
f[i][j] = max(f[i - 1][j], f[i][j - vi] + wi)

优化代码1:

#include<iostream>
using namespace std;

const int N = 1010;
int v[N], w[N];
int f[N][N];

int main()
{
    int n, m;
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= m; j++)
        {
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
        }
            
    
    cout << f[n][m] << endl;
    
    return 0;
}

一维优化

f[i][j] = f[i - 1][j];
f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);

变化为
f[j] = f[j] // 省略
f[j] = max(f[j], f[j - v[i]] + w[i])
// 由于f[i][j]需要用到第i层的结果,j-v[i]严格小于j,所以用j-v[i]更新j时,用的是第i层的结果,因此j不需要逆序,这一点与01背包代码相反。

优化代码2:

#include<iostream>
using namespace std;

const int N = 1010;
int v[N], w[N];
int f[N];

int main()
{
    int n, m;
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i++)
        for (int j = v[i]; j <= m; j++)
        {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
            
    
    cout << f[m] << endl;
    
    return 0;
}

多重背包问题1

在这里插入图片描述
思路:
多重背包问题在于每个物品选择的个数是有限个,从集合的角度分析DP问题,状态表示为:选择前i个物品,总体积小于等于j的选法的集合,属性f[i][j]表示价值的最大值。状态计算,因为每个状态可以表示为选0个,选1个…选k个,k小于等于当前物品的最大个数,即状态转移方程为:
f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i − 1 ] [ j − k ∗ v [ i ] ] + k ∗ w [ i ] ) , k ≤ s [ i ] , k ∗ v [ i ] ≤ j f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]) ,k \le s[i], k * v[i] \le j f[i][j]=max(f[i][j],f[i1][jkv[i]]+kw[i]),ks[i],kv[i]j
代码:

#include <iostream>
using namespace std;

const int N = 110;
int v[N], w[N], s[N];
int f[N][N];

int main()
{
    int n, m;
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
    
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= m; j++)
            for (int k = 0; k <= s[i] && k * v[i] <= j; k++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
                
    cout << f[n][m] << endl;
            
    return 0;
}

多重背包问题2

在这里插入图片描述
思路:
这道题的数据量更大,朴素的做法会超时,接下来要对它进行优化,我们知道,任意一个整数都可以用二进制的数来进行表示。

123 = 1 + 2 + 4 + 8 + 16 + 32 + 50
1 ~ 32可以凑出 1 ~ 63 加上 50 可以凑出 51 ~ 123,
所以可以凑出 1 ~ 123内的任意数

所以本题优化在于将一类物品分为由若干个数为二进制数的物品组合而成的大物品,再由大物品拼凑而成特定重量的物品,这样就优化为一个01背包问题了。
代码:

#include<iostream>
using namespace std;

const int N = 12010, M = 2010;

int n, m;
int v[N], w[N]; //逐一枚举最大是N*logS
int f[M]; 

int main()
{
    cin >> n >> m;
    int cnt = 0; //分组的序号
    for(int i = 1;i <= n;i ++)
    {
        int a,b,s;
        cin >> a >> b >> s;
        int k = 1; // 分组里面的个数
        while(k <= s)
        {
            cnt ++ ; 
            v[cnt] = a * k ; //分组整体体积
            w[cnt] = b * k; // 分组整体价值
            s -= k; 
            k *= 2; 
        }
        //剩余的一组
        if(s > 0)
        {
            cnt ++ ;
            v[cnt] = a * s; 
            w[cnt] = b * s;
        }
    }

    n = cnt ; 
    for(int i = 1; i <= n ;i ++)
        for(int j = m; j >= v[i]; j --)
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m] << endl;
    return 0;
}

分组背包问题

在这里插入图片描述
思路:
与01背包问题很像,区别在于分组背包里面只能选择一个物品,所以只用在01背包里再套一层循环,选一个可以使总价值最大的即可。

代码:

#include <iostream>
using namespace std;

const int N = 110;
int v[N][N], w[N][N], s[N];
int f[N];
int n, m;

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        cin >> s[i];
        for (int j = 1; j <= s[i]; j++)
            cin >> v[i][j] >> w[i][j];
    }

    for (int i = 1; i <= n; i++)
        for (int j = m; j >= 0; j--)
            for (int k = 1; k <= s[i]; k++)
                if (j >= v[i][k]) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);


    cout << f[m] << endl;

    return 0;

}

线性DP

数字三角形

在这里插入图片描述
思路(自顶向下):
从集合的角度考虑DP问题,状态表示为:从起点到[i, j]点所有路径的集合,属性dp[i][j]表示最大的数字和,每一个状态可以由该点上面的两个状态中的最大值加上该点的值f[i][j]转移过来,因此从上至下来看,状态转移方程为:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − 1 ] , d p [ i − 1 ] [ j ] ) + f [ i ] [ j ] dp[i][j] = max(dp[i - 1][j - 1], dp[i - 1][j]) + f[i][j] dp[i][j]=max(dp[i1][j1],dp[i1][j])+f[i][j]
由于数字的值有负数的情况,所以要初始化三角形及轮廓为一个很小的负数。最后结果在最后一行,取最大值即可
代码:

#include<iostream>
using namespace std;

const int N = 510, inf = -0x3f3f3f3f;
int f[N][N];
int dp[N][N];

int main()
{
    int n;
    cin >> n;

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++)
            cin >> f[i][j];
    for (int i = 0; i <= n; i++)
        for (int j = 0; j <= i + 1; j++)
            dp[i][j] = inf;

    dp[1][1] = f[1][1];

    for (int i = 2; i <= n; i++)
        for (int j = 1; j <= i; j++)
        {
            dp[i][j] = max(dp[i - 1][j - 1], dp[i - 1][j]) + f[i][j];
        }

    int res = inf;
    for (int i = 1; i <= n; i++) res = max(res, dp[n][i]);
    cout << res << endl;

    return 0;
}

思路(自底向上):
从最后一行往上加,每一个状态可以由下面的两个状态来表示,即状态转移方程为:
f [ i ] [ j ] = f [ i ] [ j ] + m a x ( f [ i + 1 ] [ j ] , f [ i + 1 ] [ j + 1 ] ) f[i][j] = f[i][j] + max(f[i+1][j], f[i+1][j+1]) f[i][j]=f[i][j]+max(f[i+1][j],f[i+1][j+1])
代码:

#include<iostream>
using namespace std;

const int N = 510;
int f[N][N];

int main()
{
    int n;
    cin >> n;

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++)
            cin >> f[i][j];

    for (int i = n - 1; i >= 1; i--)
        for (int j = i; j >= 1; j--)
            f[i][j] += max(f[i + 1][j], f[i + 1][j + 1]);

    cout << f[1][1] << endl;

    return 0;
}

最长上升子序列

在这里插入图片描述
思路:
从集合的角度考虑DP问题,状态表示为:所有以第i个数字结尾的上升子序列,属性f[i]表示所有以第i个数字结尾的上升子序列的长度的最大值。枚举每个数字为结尾,接着枚举以该数字为结尾的上升子序列,如果结尾大于子序列中的值,就对当前状态进行转移,状态转移方程为:
f [ i ] = m a x ( f [ i ] , f [ j ] + 1 ) , a [ i ] > a [ j ] f[i] = max(f[i], f[j] + 1), a[i] \gt a[j] f[i]=max(f[i],f[j]+1),a[i]>a[j]
在枚举结尾的过程中保存上升子序列长度的最大值:
代码:

#include<iostream>
using namespace std;

const int N = 1010;
int a[N], f[N];

int main()
{
    int n, ans = 1;
    cin >> n;
    
    for (int i = 1; i <= n; i++) cin >> a[i];
    
    for (int i = 1; i <= n; i++)
    {
    	// 初始情况 长度为1
        f[i] = 1;
        for (int j = 1; j < i; j++)
        {
            if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
        }
        ans = max(ans, f[i]);
    }
    
    cout << ans << endl;
    
    return 0;
}

最长公共子序列

在这里插入图片描述
思路:
从集合的角度考虑DP问题,状态表示为:字符串A前i个字符,字符串B前j个字符组成的公共子序列,属性f[i][j]表示最长公共子序列的长度的最大值。
状态计算:每一个状态可以由字符串A前i-1个字符,字符串B前j-1个字符;字符串A前i-1个字符,字符串B前j个字符;字符串A前i个字符,字符串B前j-1个字符转移过来,如果a[i] == b[j]f[i][j] = f[i - 1][j - 1] + 1,否则取f[i - 1][j]、f[i][j - 1]的最大值,状态转移方程为:
f [ i ] [ j ] = { f [ i − 1 ] [ j − 1 ] + 1 a [ i ] = b [ j ] m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] ) a [ i ] ≠ b [ j ] f[i][j]= \begin{cases} f[i-1][j-1] + 1 & a[i] = b[j] \\ max(f[i-1][j], f[i][j-1]) & a[i] \neq b[j] \\ \end{cases} f[i][j]={f[i1][j1]+1max(f[i1][j],f[i][j1])a[i]=b[j]a[i]=b[j]
代码

#include<iostream>
using namespace std;

const int N = 1005;
char a[N], b[N];
int f[N][N];
int n, m;

int main()
{
    cin >> n >> m >> a + 1 >> b + 1;
    
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
            else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
        }
        
    }
    
    cout << f[n][m] << endl;
    
    return 0;
}

最短编辑距离

在这里插入图片描述
思路:
从集合的角度考虑DP问题,状态表示为所有将a[1~i]变化为b[1~j]的操作方式的集合,属性f[i][j]表示操作次数的最小值。因为增添字符、删除字符都是相对而言的,所以可以以一个字符串作为基准,不妨以字符串a作为基准。对于增加操作,字符串a通过增加一个字符变化为字符串b,则a[1 ~ i] == b[1 ~ j - 1],对应操作次数为f[i][j - 1] + 1;对于删除操作,字符串a通过增加一个字符变化为字符串b,则a[1 ~ i - 1] == b[1 ~ j ],对应操作次数为f[i - 1][j] + 1;对于修改操作,字符串a通过修改一个字符变化为字符串b,如果a[i] == b[j]不用修改,对应操作次数为f[i - 1][j - 1],否则需要修改,对应操作次数为f[i - 1][j - 1] + 1。每一个状态都可以从上面三个状态转移过来,因此状态转移方程为:
f [ i ] [ j ] = m i n ( f [ i ] [ j − 1 ] + 1 , f [ i − 1 ] [ j ] + 1 , f [ i − 1 ] [ j − 1 ] + ( a [ i ] ≠ b [ j ] ) ) f[i][j] = min(f[i][j - 1] + 1, f[i - 1][j] + 1,f[i - 1][j - 1] + (a[i]\ne b[j])) f[i][j]=min(f[i][j1]+1,f[i1][j]+1,f[i1][j1]+(a[i]=b[j]))
在这里插入图片描述

代码:

#include<iostream>
using namespace std;

const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];

int main()
{
    cin >> n >> a + 1;
    cin >> m >> b + 1;
    
    // 如果a空 只能通过添加字符使 a == b, b的长度为几,就有几个添加字符的操作
    for (int i = 0; i <= m; i++) f[0][i] = i;
    // 如果b空 只能通过删除字符使 a == b, a的长度为几,就有几个删除字符的操作
    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++)
        {
            //  a[1,i-1] = b[1,j] a通过删除字符 a[1,i] = b[1,j-1] a通过增加字符
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            // 修改字符 若a[i] == b[j] 不用修改
            if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
            // 修改字符的操作 + 1
            else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
        }
    }
    
    cout << f[n][m] << endl;
    
    return 0;
}

编辑距离

在这里插入图片描述

思路:
根据上一道题的思路,封装成一个计算编辑距离的函数,计算询问中的每个字符串与给定字符串的编辑距离,如果小于等于给定的操作次数,计数器+1,最后输出结果即可。
代码:

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

const int N = 15, M = 1010;
int f[N][N];
char str[M][N];
int n, m;

int edit_dist(char a[], char b[])
{
    int la = strlen(a + 1), lb = strlen(b + 1);
    // 如果a空 a只能通过添加字符使 a == b, b的长度为几,就有几个添加字符的操作
    for (int i = 0; i <= lb; i++) f[0][i] = i;
    // 如果b空 a只能通过删除字符使 a == b, a的长度为几,就有几个删除字符的操作
    for (int i = 0; i <= la; i++) f[i][0] = i;    
    
    for (int i = 1; i <= la; i++)
    {
        for (int j = 1; j <= lb; j++)
        {
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
        }
    }
    
    return f[la][lb];
    
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%s", str[i] + 1);
    
    while (m--)
    {
        char s[N];
        int limit;
        scanf("%s%d", s + 1, &limit);
        
        int res = 0;
        for (int i = 1; i <= n; i++) 
            if (edit_dist(str[i], s) <= limit)
                res++;
        printf("%d\n", res);
    }
    
    return 0;
}

区间DP

石子合并

在这里插入图片描述
思路:
从集合的角度考虑DP问题,状态表示为:从i到j堆石子的所有合并方式,属性f[i][j]表示从 i i i j j j堆石子合并的最小代价。状态计算:根据每个不同合并石子堆的分界线可以划分出不同的状态。合并从i到j堆石子的状态可以转移为合并从i到k堆石子,合并从 k + 1 k+1 k+1 j j j堆石子,合并这两堆石子的代价之和,其中k表示分界线, k ∈ [ i , j ) k \in [i,j) k[i,j),可以用前缀和数组处理区间和,最后得到状态转移方程:
f [ i ] [ j ] = m i n ( f [ i ] [ k ] + f [ k + 1 ] [ j ] + s [ j ] − s [ l − 1 ] ) , k ∈ [ i , j ) f[i][j] = min(f[i][k]+f[k+1][j]+s[j]-s[l-1]),k \in [i,j) f[i][j]=min(f[i][k]+f[k+1][j]+s[j]s[l1]),k[i,j)
在这里插入图片描述
区间DP问题一般都要枚举区间的长度。
代码

#include<iostream>
using namespace std;

const int N = 310;
int s[N], f[N][N], a[N];

int main()
{
    int n;
    cin >> n;
    
    
    for (int i = 1; i <= n; i++) cin >> a[i], s[i] = s[i - 1] + a[i];
    
    //len 表示区间长度
    for (int len = 2; len <= n; len++)
    {
        for (int i = 1; i + len - 1 <= n; i++)
        {
            int l = i, r = i + len - 1;
            f[l][r] = 1e8;
            for (int k = l; k < r; k++)
                // k表示分界线
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }
    }
            
    cout << f[1][n] << endl;
    
    return 0;
}

计数类DP

整数划分

在这里插入图片描述
思路(完全背包):
本题可以看作体积为 n n n,物品重量为 [ 1 , n ] [1,n] [1,n] n n n件物品的完全背包问题。状态表示为:从1到i当中选,总体积恰好为j的所有方案的集合。f[i][j]表示所有选法的数量。集合的划分可以根据第i个物品选择多少个,把所有集合的方案数相加,得到朴素状态转移方程:
f [ i ] [ j ] = f [ i ] [ j ] + f [ i − 1 ] [ j − k ∗ i ] , k × i ≤ j f[i][j] = f[i][j] + f[i - 1][j - k * i], k×i \le j f[i][j]=f[i][j]+f[i1][jki],k×ij
在这里插入图片描述

代码:

#include<iostream>
using namespace std;

const int N = 1010, mod = 1e9 + 7;

int n;
int f[N][N];

int main()
{
    scanf("%d", &n);
    f[0][0] = 1;
    
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= n; j++)
            for (int k = 0; k * i <= j; k++)
                f[i][j] = (f[i][j] + f[i - 1][j - k * i]) % mod;
            
    printf("%d", f[n][n]);
    
    return 0;
}

优化代码1

#include<iostream>
using namespace std;

const int N = 1010, mod = 1e9 + 7;

int n;
int f[N][N];

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

优化代码2

#include<iostream>
using namespace std;

const int N = 1010, mod = 1e9 + 7;

int n;
int f[N];

int main()
{
    scanf("%d", &n);
    f[0] = 1;
    
    for (int i = 1; i <= n; i++)
        for (int j = i; j <= n; j++)
            f[j] = (f[j] + f[j - i]) % mod;
            
    printf("%d", f[n]);
    
    return 0;
}

状态压缩DP

最短Hamilton路径

在这里插入图片描述
思路
首先想下暴力算法,这里直接给出一个例子。
比如数据有 5 5 5 个点,分别是 0 , 1 , 2 , 3 , 4 0,1,2,3,4 0,1,2,3,4
那么在爆搜的时候,会枚举一下六种路径情况(只算对答案有贡献的情况的话):

c a s e 1 : 0 → 1 → 2 → 3 → 4 case 1: 0→1→2→3→4 case1:01234
c a s e 2 : 0 → 1 → 3 → 2 → 4 case 2: 0→1→3→2→4 case2:01324
c a s e 3 : 0 → 2 → 1 → 3 → 4 case 3: 0→2→1→3→4 case3:02134
c a s e 4 : 0 → 2 → 3 → 1 → 4 case 4: 0→2→3→1→4 case4:02314
c a s e 5 : 0 → 3 → 1 → 2 → 4 case 5: 0→3→1→2→4 case5:03124
c a s e 6 : 0 → 3 → 2 → 1 → 4 case 6: 0→3→2→1→4 case6:03214

那么观察一下 c a s e 1 case 1 case1 c a s e 3 case 3 case3,可以发现,我们在计算从点 0 0 0 到点 3 3 3的路径时,其实并不关心这两中路径经过的点的顺序,而是只需要这两种路径中的较小值,因为只有较小值可能对答案有贡献。
所以,我们在枚举路径的时候,只需要记录两个属性:当前经过的点集,当前到了哪个点。
而当前经过的点集不是一个数。观察到数据中点数不会超过 20 20 20,我们可以用一个二进制数表示当前经过的点集。其中第 i i i位为 1/0 表示是/否经过了点 i i i
然后用闫式 d p dp dp 分析法考虑 d p dp dp
状态表示: f [ s t a t e ] [ j ] f[state][j] f[state][j]。其中 s t a t e state state 是一个二进制数,表示点集的方法如上述所示。

  • 集合:经过的点集为 s t a t e state state,且当前到了点 j j j 上的所有路径。
  • 属性:路径总长度的最小值

状态计算:假设当前要从点 k k k 转移到 j j j。那么根据 H a m i l t o n Hamilton Hamilton 路径的定义,走到点 k k k 的路径就不能经过点 j j j,所以就可以推出状态转移方程 f [ s t a t e ] [ j ] = m i n ( f [ s t a t e ] [ j ] , f [ s t a t e − ( 1 < < j ) ] [ k ] + w [ k ] [ j ] ) f[state][j] = min(f[state][j], f[state - (1 << j)][k] + w[k][j]) f[state][j]=min(f[state][j],f[state(1<<j)][k]+w[k][j])
其中w[k][j]表示从点 k k k 到点 &j& 的距离
state - (1 << j)表示从点集中去除到 j j j的点

所有状态转移完后,根据 f [ s t a t e ] [ j ] f[state][j] f[state][j]的定义,要输出 f [ 111 ⋯ 11 ( n 个 1 ) ] [ n − 1 ] f[111⋯11(n个1)][n−1] f[11111(n1)][n1]
那么怎么构造 n n n 1 1 1 呢,可以直接通过 1 << n 求出 100 ⋯ 0 ( n 个 0 ) 100⋯0(n个0) 1000(n0),然后减一即可。

时间复杂度
枚举所有 s t a t e state state 的时间复杂度是 O ( 2 n ) O(2^n) O(2n)
枚举 j j j 的时间复杂读是 O ( n ) O(n) O(n)
枚举 k k k 的时间复杂度是 O ( n ) O(n) O(n)
所以总的时间复杂度是 O ( n 2 2 n ) O(n^22^n) O(n22n)
代码:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 20, M = 1 << N;
// f[i][j]所有从0到j,走过的路径包括i的路径 i为路径的集合
int w[N][N], f[M][N];

int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cin >> w[i][j];
            
    memset(f, 0x3f, sizeof f);
    // 表示从0到0    0,2,3,4状压为11101
    f[1][0] = 0;
    
    for (int i = 0; i < 1 << n; i++)
        for (int j = 0; j < n; j++)
        {
            // i中包括到j的路径
            if (i >> j & 1)
            {
                for (int k = 0; k < n; k++)
                {
                    // i中去除到j的路径 包括k的路径
                    if ((i - (1 << j)) >> k & 1)
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
                }
            }
        }
        
    cout << f[(1 << n) - 1][n - 1] << endl;
    
    return 0;
}

树形DP

没有上司的舞会

在这里插入图片描述
思路:
根据闫式DP分析法的思想,状态表示为: f [ u ] [ 0 ] f[u][0] f[u][0]表示从以 u u u为根节点的子树中选择,并且不包括 u u u的选法,则 f [ u ] [ 1 ] f[u][1] f[u][1]表示从以 u u u为根节点的子树中选择,并且包括 u u u的选法。 f [ u ] [ 0 ] f[u][0] f[u][0] f [ u ] [ 1 ] f[u][1] f[u][1]值为结果的最大值。再来计算状态转移, f [ u ] [ 0 ] f[u][0] f[u][0]可以由其子孙节点 j j j转移来,由于不包括 u u u,可以从 f [ j ] [ 0 ] f[j][0] f[j][0] f [ j ] [ 1 ] f[j][1] f[j][1],转移到 f [ u ] [ 0 ] f[u][0] f[u][0] f [ u ] [ 1 ] f[u][1] f[u][1],由于包括 u u u,所以只能由 f [ j ] [ 0 ] f[j][0] f[j][0]转移过来。则状态转移方程为
{ f [ u ] [ 0 ] = f [ u ] [ 0 ] + m a x ( f [ j ] [ 0 ] , f [ j ] [ 1 ] ) f [ u ] [ 1 ] = f [ u ] [ 1 ] + f [ j ] [ 0 ] \begin{cases} f[u][0] = f[u][0] + max(f[j][0], f[j][1]) \\ f[u][1] = f[u][1] + f[j][0] \end{cases} {f[u][0]=f[u][0]+max(f[j][0],f[j][1])f[u][1]=f[u][1]+f[j][0]
枚举所有节点时间复杂度等于边的个数,时间复杂度为 O ( n ) O(n) O(n),也是本题的时间复杂度。
在这里插入图片描述
在这里插入图片描述
代码:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 6010;
int happy[N];
int h[N], e[N], ne[N], idx;
int f[N][2];
bool has_father[N];

//邻接表存储图
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u)
{
    f[u][1] = happy[u];
    
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dfs(j);
        f[u][0] += max(f[j][0], f[j][1]);
        f[u][1] += f[j][0];
    }
}

int main()
{
    
    int n;
    cin >> n;
    
    for (int i = 1; i <= n; i++) cin >> happy[i];
    
    memset(h, -1, sizeof h);
    
    for (int i = 0; i < n - 1; i++)
    {
        int l, k;
        cin >> l >> k;
        has_father[l] = true;
        add(k, l);
    }
    
    // 寻找根节点
    int root = 1;
    while(has_father[root]) root++;
    
    dfs(root);
    
    cout << max(f[root][0], f[root][1]) << endl;
    
    return 0;
}

记忆化搜索

有关记忆化搜索的介绍:https://blog.csdn.net/hjf1201/article/details/78680814

滑雪

在这里插入图片描述
思路:
根据根据闫式DP分析法的思想,状态表示为:所有从 ( i , j ) (i,j) (i,j)开始划的路径, f [ i ] [ j ] f[i][j] f[i][j]表示路径长度的最大值。每一个状态都可以由上下左右四个方向转移过来,因此可以用方向数组表示转移的过程,若某个方向的高度低于当前状态的高度,则可以转移过来,每找到一个路径的长度就直接返回。由于高度存在为0的值, f f f初始化为-1。
在这里插入图片描述
代码:

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

const int N = 310;
int h[N][N];
int f[N][N];
int dx[4] = {-1, 0, 1 ,0}, dy[4] = {0, 1, 0, -1};
int n, m;

int dp(int x, int y)
{
    int &v = f[x][y];
    // 不为-1,也就是计算过的 直接返回
    if (~v) return v;
    
    // 计算路径长度 初始化为1
    v = 1;
    for (int i = 0; i < 4; i++)
    {
        int a = x + dx[i], b = y + dy[i];
        // 不出边界 并且下一个方向的高度低于当前方向的
        if (a >= 1 && a <= n && b >= 1 && b <= m && h[a][b] < h[x][y])
        // 下一个方向的路径长度加1,取最大
            v = max(v, dp(a, b) + 1);
    }
    
    // 返回当前路径长度
    return v;
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> h[i][j];
            
    memset(f, -1, sizeof f);
    int res = 0;
    // 由于不知道那个点为起点 所以依次进行计算
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            res = max(res, dp(i, j));
            
    cout << res << endl;
    
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/112988.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

正交编码器数字滤波器(二)

正交编码器数字滤波器&#xff08;一&#xff09;电路初画完了&#xff0c;正交编码器数字滤波器&#xff08;二&#xff09;把核心部分用HDL描述语言实现了&#xff0c;放在一个小芯片里。 上面的整张图上&#xff0c;截出下面的小图&#xff0c;就用古老的ABEL工具实现它。 这…

高斯函数和C++简单实现

高斯函数在科学和工程中有广泛应用&#xff1b;其定义为&#xff0c; 其一般图像为&#xff0c; 高斯函数的图形在形状上像一个倒悬着的钟&#xff1b;参数a指高斯曲线的峰值&#xff0c;b为其对应的横坐标&#xff0c;c即标准差&#xff08;有时也叫高斯RMS宽值&#xff09;&a…

【1754. 构造字典序最大的合并字符串】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给你两个字符串 word1 和 word2 。你需要按下述方式构造一个新字符串 merge &#xff1a;如果 word1 或 word2 非空&#xff0c;选择 下面选项之一 继续操作&#xff1a; 如果 word1 非空&#xff0…

Python常用基础语法知识点大全

介绍 Python 是一门独特的语言&#xff0c;快速浏览一下他的要点&#xff1a; 面向对象&#xff1a;每一个变量都是一个类&#xff0c;有其自己的属性&#xff08;attribute&#xff09;与方法&#xff08;method&#xff09;。语法块&#xff1a;用缩进&#xff08;四个空格…

Qml 中用 Shader 实现圣诞树旋转灯

一、前言 2022年圣诞节到来啦&#xff0c;很高兴这次我们又能一起度过~ 这次给大家带来一个简单漂亮圣诞树灯。 当然了&#xff0c;本篇文章主要是讲解一下如何在 Qml 中使用 GLSL 来实现自己的特效。 至于代码嘛&#xff0c;我比较喜欢在 Shaderjoy 上寻找&#xff0c;那里有很…

Biotin-PEG-Biotin,生物素-聚乙二醇-生物素聚乙二醇试剂供应

一&#xff1a;产品描述 1、名称 英文&#xff1a;Biotin-PEG-Biotin 中文&#xff1a;生物素-聚乙二醇-生物素 2、CAS编号&#xff1a;N/A 3、所属分类&#xff1a;Biotin PEG 4、分子量&#xff1a;可定制&#xff0c;2000/10000/3400/1000/20000/500 5、质量控制&…

c++继承知识点

目录1.继承的概念及定义1.1继承的概念1.2 继承定义1.2.1定义格式1.2.2继承关系和访问限定符1.2.3继承基类成员访问方式的变化2.基类和派生类对象赋值转换3.继承中的作用域4.派生类的默认成员函数5.继承与友元6. 继承与静态成员如何定义一个不被继承的类7.继承的一个题目8. 复杂…

目标检测之YOLOv2算法分析

要点 Batch Normalization 训练 若batchsize64,某一层的某一个神经元会输出64个响应值&#xff0c;对这64个响应值求均值&#xff0c;标准差&#xff0c;然后标准化&#xff0c;对标准化的结果乘λβ\lambda \betaλβ,其中λ\lambdaλ和 β\betaβ是需要训练的参数&#xf…

Windows平台RTMP、RTSP播放器录像模块精细化控制

技术背景 上篇文章&#xff0c;我们介绍了Unity平台RTMP、RTSP播放器录像功能&#xff0c;这里&#xff0c;我们详细的介绍下&#xff0c;做个RTSP或RTMP拉流端录像模块有哪些需要考虑的技术点&#xff1f; 在我们常规的考量&#xff0c;RTMP或RTSP流录制&#xff0c;无非就是…

在gitee上新建仓库并上传文件

一、进入到自己gitee的个人主页&#xff0c;点击图示新建仓库 二、根据图示操作&#xff0c;最后点击创建 三、如果没有配置git全局设置&#xff0c;需要配置一下(配置过的可以跳过这一步) 四、打开你要上传的文件&#xff0c;在里面右击鼠标&#xff0c;点击如图所示 五、输入…

spring之Bean的循环依赖问题

文章目录一、Bean的循环依赖之Set注入模式下1、Husband类2、Wife类3、Spring配置文件4、测试类5、测试结果6、结论二、Bean的循环依赖之构造方法注入模式下1、Husband类2、Wife类3、Spring配置文件4、测试类5、运行结果三、Spring解决循环依赖的机理三级缓存&#xff08;面试常…

PyQt5 基本布局管理 及 信号槽机制

一&#xff1a;布局设计 & 信号槽机制 效果实现如下&#xff1a; 对于窗口整体设计左右布局 对于左边布局&#xff0c;包括有水平布局(用户信息 左上方一块)垂直布局(多个按钮 左下方一块) 对于右边布局&#xff0c;主要是窗口切换&#xff0c;通过按下左边布局的左下方侧按…

SQLAlchemy连接MySQL及记录的查询、更新、删除、多表关联查询

SQLAlchemy是Python的ORM库&#xff0c;支持多种数据库。 建立连接 连接MySQL要用到Engine&#xff0c;Engine集成了连接池pool和方言Dialect&#xff08;支持不通数据库的SQL语法&#xff09;&#xff0c;最后都统一成标准DBAPI。 from sqlalchemy import create_engine en…

TypeScript

现在说起TypeScript想必大家都不会陌生的&#xff0c;当初从碎片信息中了解TypeScript&#xff0c;我认为他的变量声明和Rust语言有几分相似&#xff0c;是一门比较严格的语言&#xff0c;今天正式的来学习他 JavaScript易学习&#xff0c;易用&#xff0c;以至于大多数人对于…

软件体系结构 思维导图

软件体系结构 思维导图 软件体系结构思维导图 源文件放在 GitHub 仓库 使用 Xmind 即可打开查看 课程评价 比较抽象和理论化&#xff0c;如果光看 PPT 肯定看不懂&#xff0c;得听课或者看视频 后面实验试图基于 SpringBoot 去实战教学&#xff0c;可惜没系统学过只能照搬…

Kafka Consumer开发

Kafka Consumer - 消费者 跟生产者一样&#xff0c;消费者也属于kafka的客户端&#xff0c;不过kafka消费者是从kafka读取数据的应用&#xff0c;侧重于读数据。一个或多个消费者订阅kafka集群中的topic&#xff0c;并从broker接收topic消息&#xff0c;从而进行业务处理。今天…

一种嵌入式项目的参数保存方案

设计背景 嵌入式项目中&#xff0c;为了保证系统的正常运转&#xff0c;通常需要保存一部分数据至非易失存储设备如flash中。此处提供了一种通用的方案用于快速在项目中集成参数保存功能&#xff0c;该方案有以下几点特征&#xff1a; 接口简便&#xff0c;方便快速集成以及使用…

东北大学2023分布式操作系统实验

1.实验目的 建立伪分布式&#xff08;有条件的可以建立分布式环境&#xff09;的Hadoop环境&#xff0c;并成功运行示例程序。 2.Hadoop简介 2.1 Hadoop项目基础结构 在其核心&#xff0c;Hadoop主要有两个层次&#xff0c;即&#xff1a; 加工/计算层(MapReduce)存储层(Ha…

Python pandas有几千个库函数,你用过几个?(1)

对Python的 pandas 库所有的内置元类、函数、子模块等全部浏览一遍&#xff0c;然后挑选一些重点学习一下。我安装的库版本号为1.3.5&#xff0c;如下&#xff1a; >>> import pandas as pd >>> pd.__version__ 1.3.5 >>> print(pd.__doc__)pandas…

C++ STL vector list set map容器循环通过迭代器删除元素注意事项

先说说写这篇博客的原因吧&#xff0c;同事转部门了&#xff0c;把他手头的工作交接给了我。他以前维护的一个模块&#xff0c;会将外部输入的数据缓存起来分段处理&#xff0c;处理完了就会清除缓存数据&#xff0c;最近出现了一个bug&#xff0c;缓存数据一直不清除&#xff…