Acwing 第五章 动态规划(一)

Acwing 第五章 动态规划(一)

一、01背包问题

n个物品,容量为v的背包,每个物品有两个属性:体积vi,价值wi,每件物品只能用一次,目标求出最大价值是多少?
在这里插入图片描述
f(i,j) 从1 - i 个物品中选,总体积不超过 j
集合:表示所有的选法

状态计算

在这里插入图片描述

#include<iostream>
using namespace std;
const int M = 1005;
int n,m;//物品数量和背包体积
int v[M],w[M];//表示某件物品的体积和价值
int f[M][M];//状态表示:包含所有选法的集合
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    cin>>v[i]>>w[i];
    //f[0][0~M]全部初始化为0
    for(int i=1;i<=n;i++) //枚举前i件物品
    {
        for(int j=1;j<=m;j++)//枚举不超过j的体积
        {
            if(v[i] > j) f[i][j] = f[i-1][j];//当前背包容量装不进第i个物品,则价值等于前i-1个物品
            else f[i][j] = max(f[i-1][j],f[i-1][j-v[i]]+w[i]);//能装,需进行决策是否选择第i个物品
        }
    }
    cout<<f[n][m]<<endl;
}

二、完全背包问题

n个物品,容量为v的背包,每个物品有两个属性:体积vi,价值wi,每件物品可以用无限次,目标求出最大价值是多少?
在这里插入图片描述

朴素做法:超时

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010;
int v[N],w[N];
int f[N][N];
int n,m;
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++)
        {
            for(int k = 0;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;
}

优化做法

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010;
int v[N],w[N];
int f[N];
int n,m;
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 = v[i];j <= m;j++)//优化版必须从v[i]开始枚举
        {
            f[j] = max(f[j],f[j - v[i]] + w[i]);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

三、多重背包问题

n个物品,容量为v的背包,每个物品有两个属性:体积vi,价值wi,每件物品最多有si个(每件物品的si不一样),目标求出最大价值是多少?

朴素做法(0<N,V≤100,0<vi,wi,si≤100)

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int v[N],w[N],s[N];
int f[N][N];
int n,m;
int main()
{
    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;
}

优化版(0<N≤1000,0<V≤2000,0<vi,wi,si≤2000)

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 25005; //最多会拆分出1000*log2000个物品
int n,m,v[N],w[N];
int a,b,s;//某件物品的体积,价值和个数
int f[N];//表示所有选法的集合
int main()
{
    cin>>n>>m;
    int cnt = 0;
    for(int i = 1;i <= n;i++)
    {
        cin>>a>>b>>s;
        int k = 1;//当前2的某次幂,用于把物品尽量拆分成2的次幂的个数
        while(k <= s)
        {
            cnt++; //维护当前拆分的所有物品的个数
            v[cnt] = a * k;//k个物品,每个占据a体积
            w[cnt] = b * k;//k个物品,每个物品有b价值
            s -= k;//维护当前未拆分的物品个数
            k = k << 1;//k向左移位
        }
        if(s > 0)//剩下的物品个数不能被拆分成更大的2的次幂,故全部分为一组
        {
            cnt++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }
    n = cnt;//更新当前拆分后物品的个数
    
    for(int i = 1;i <= n;i++) //01背包优化版
    {
        for(int j = m;j >= v[i];j--)//优化后必须从m开始递减枚举
        {
            f[j] = max(f[j],f[j - v[i]] + w[i]);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

四、分组背包问题

n组物品,每次物品里有若干个,每个组里面最多选一个物品,容量为v的背包,每个物品有两个属性:体积vi,价值wi,每件物品只能用一次,目标求出最大价值是多少?

第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int v[N][N],w[N][N];//第i组第j个物品的体积和价值
int s[N];//每一组的物品个数
int f[N];//所有选法集合
int n,m;
int main()
{
    cin>>n>>m;
    for(int i = 1;i <= n;i++)
    {
        cin>>s[i];//第i组物品个数
        for(int j = 1;j<=s[i];j++)
        {
            cin>>v[i][j]>>w[i][j];//第i组第j个物品的体积和价值
        }
    }
    for(int i = 1;i <= n;i++)//枚举组的序号
    {
        for(int j = m;j >= 1;j--)//优化后必须从m开始递减枚举背包体积
        {
            for(int k = 1;k <= s[i];k++)//枚举某组中第k个物品
            {
                if(v[i][k] <= j)//若第i组第k件物品的体积小于背包体积
                {
                    f[j] = max(f[j],f[j - v[i][k]] + w[i][k]);//价值较大才选择
                }
            }
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

五、线形DP

898. 数字三角形
image
O(N^2)

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 505,INF = 0x3f3f3f3f;
int a[N][N];//数字三角形
int f[N][N]; //状态表示:当前第i行第j列数字和的最大值
int n;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=i;j++)
        {
            cin>>a[i][j];
        }
    }
    memset(f,-INF,sizeof f);//必须把方案表示数组全部初始化成负无穷
    //因为在状态计算时,计算边界数字时会发生越界,负无穷可以解决此类问题
    f[1][1] = a[1][1];//第一个数的和最大值一定是本身 
    for(int i=1;i<=n;i++) //从第二行开始枚举计算
    {
        for(int j=1;j<=i;j++)
        {
            f[i][j] = max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);//取左上方路径与右上方路径数字和的最大值加其本身就是答案
        }
    }
    int Max = -0x3f3f3f3f;
    for(int i=1;i<=n;i++)
    {
        Max = max(Max,f[n][i]);//答案取最后一行最大值
    }
    cout<<Max<<endl;
    return 0;
}

895. 最长上升子序列
O(N^2)
image

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1005;
int n,a[N];
int f[N];
int main()
{
    cin>>n;
    for(int i = 1;i <= n;i++)
    {
        cin>>a[i];
    }
    for(int i = 1;i <= n;i++)
    {
        f[i] = 1; //初始化,集合里只有a[i]这一个数
        for(int j = 1;j < i;j++) //从1~i-1枚举,若j比i小,则根据个数考虑将f[j]集合与a[i]合并
        {
            if(a[j] < a[i]) 
            {
                f[i] = max(f[i],f[j] + 1);
            }
        }
    }
    int res = -1;
    for(int i = 1;i<=n;i++) //比较每一个集合,选取个数最多的那个
    {
        res = max(res,f[i]);
    }
    cout<<res<<endl;
    return 0;
}

求具体方案的集合元素

for(int i = 1;i <= n;i++)
    {
        f[i] = 1; //初始化,集合里只有a[i]这一个数
        g[i] = 0;//表示集合里只有a[i]本身
        for(int j = 1;j < i;j++) //从1~i-1枚举,若j比i小,则根据个数考虑将f[j]集合与a[i]合并
        {
            if(a[j] < a[i]) 
            {
                if(f[i] < f[j] + 1)
                {
                    f[i] = f[j] + 1;
                    g[i] = j;    
                }
            }
        }
    }
    int res = -1,k;
    for(int i = 1;i <= n;i++) //比较每一个集合,选取个数最多的那个
    {
        if(res < f[i])
        {
            res = f[i];
            k = i;
        }
    }
    while(k!=0)
    {
        printf("%d ",a[k]);
        k = g[k];//递推
    }
    puts("");
    cout<<res<<endl;

896. 最长上升子序列 II
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤100000,
−109≤数列中的数≤109
单调栈+二分(O(NlogN))

#include<iostream>
using namespace std;
const int N = 1e5+10;
int a[N],stk[N],top=0;
int n;

int find(int x) //二分查找单调栈中最小的大于等于x的数
{
    int l = 1,r = top;
    while(l <= r)
    {
        int mid = l+r>>1;
        if(stk[mid] >= x)
        {
            r = mid - 1;
        }
        else l = mid + 1;
    }
    return l;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    stk[++top] = a[0]; //第一个数入栈
    for(int i = 1;i < n;i++)
    {
        if(a[i] > stk[top]) stk[++top] = a[i]; //若当前数大于栈顶,入栈
        else //否则找到栈中大于等于a[i]的最小的数,用a[i]替换
        {
            int p = find(a[i]);
            stk[p] = a[i];
        }
    }
    // for(int i = 1;i<=top;i++)
    // {
    //     cout<<stk[i]<<" ";
    // }
    cout<<top<<endl; //输出栈中元素个数
    return 0;
}

902. 最短编辑距离
image

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

using namespace std;
const int N = 1005;
char a[N],b[N];
int f[N][N];//a串前i个字母通过变换操作变成b串前j个字母的最小操作次数
int n,m;

int main()
{
    cin>>n>>a+1;
    cin>>m>>b+1;
    //初始化操作,处理边界问题
    for(int i = 1;i <= n;i++)
    {
        f[i][0] = i; //把a串中前i个字母变成0,需要i次删除操作
    }
    for(int i = 1;i <= m;i++)
    {
        f[0][i] = i;//把a串中空字串变成b中前i个字母,需要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); //左边表示删除操作,前提是a[1~i-1]等于b[1~j];
            //右边表示添加操作,前提是a[1~i]等于b[1~j-1]
            if(a[i] == b[j]) f[i][j] = min(f[i][j],f[i-1][j-1]);//相等不需要替换,前提是a[1~i-1]等于b[1~j-1]
            else f[i][j] = min(f[i][j],f[i-1][j-1] + 1); //不相等需要替换操作,前提是a[1~i-1]等于b[1~j-1]
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

897. 最长公共子序列
image
image
如果两个字符相等,就可以直接转移到f[i-1][j-1],不相等的话,两个字符一定有一个可以抛弃,可以对f[i-1][j],f[i][j-1]两种状态取max来转移。

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1005;
char a[N],b[N];
int n,m;
int f[N][N]; //集合表示:第一个串的前i个字母中,第二个串的前j个字母中的公共子序列,属性:序列长度的最大值

int main()
{
    cin>>n>>m;
    scanf("%s%s",a+1,b+1);
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            if(a[i] == b[j]) //第1个串的第i个字母与第二个串的第j个字母相等,就可以直接转移到f[i-1][j-1]
            {
                f[i][j] = max(f[i][j],f[i-1][j-1] + 1);
            } //不相等的话,两个字符一定有一个可以抛弃,可以对f[i-1][j],f[i][j-1]两种状态取max来转移。
            else f[i][j] = max(f[i][j-1],f[i-1][j]);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

六、区间DP

282. 石子合并
image

思路

f[i,j]表示将第i堆到第j堆所有合并成一堆的最小代价
状态计算:所有第i堆到第j堆合并成一堆的合并方式分成若干类,以最后一步合并的分界线
左边的最小代价+右边的最小代价+最后一步的代价
image

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 305,INF = 0x3f3f3f3f;
int f[N][N],a[N],s[N];
int n;
int main()
{
    cin>>n;
    for(int i = 1;i <= n;i++)
    {
        cin>>a[i];
        s[i] = s[i-1] + a[i]; // 计算前缀和
    }
    //由于状态递推要求计算时已求出子区间的结果,因此必须外层从小到大枚举区间长度
    for(int len = 2;len <= n;len++) //区间长度为1时,不需要合并,代价为0,故长度从2开始枚举
    {
        for(int i = 1;i + len - 1 <= n;i++)
        {
            int l = i, r = i + len - 1;//左右边界
            f[l][r] = INF;//计算最小值,故初始化f[l][r]必须为无穷大
            for(int k = l;k < r;k++) //分界点枚举 [l,r-1]
            {
                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

338. 计数问题
count(n,x):1-n中x出现的次数
从a到b中x出现的总次数 = count(b,x) - count(a-1,x)
分类讨论
image

# include <iostream>
# include <cmath>
using namespace std;
int dgt(int n) // 计算整数n有多少位
{
    int res = 0;
    while (n) ++ res, n /= 10;
    return res;
}

int cnt(int n, int i) // 计算从1到n的整数中数字i出现多少次 
{
    int res = 0, d = dgt(n);
    for (int j = 1; j <= d; ++ j) // 从右到左第j位上数字i出现多少次
    {
        // l和r是第j位左边和右边的整数 (视频中的abc和efg); dj是第j位的数字
        int p = pow(10, j - 1), l = n / p / 10, r = n % p, dj = n / p % 10;
        // 计算第j位左边的整数小于l (视频中xxx = 000 ~ abc - 1)的情况
        if (i) res += l * p; 
        if (!i && l) res += (l - 1) * p; // 如果i = 0, 左边高位不能全为0(视频中xxx = 001 ~ abc - 1)
        // 计算第j位左边的整数等于l (视频中xxx = abc)的情况
        if ( (dj > i) && (i || l) ) res += p;
        if ( (dj == i) && (i || l) ) res += r + 1;
    }
    return res;
}

int main()
{
    int a, b;
    while (cin >> a >> b , a)
    {
        if (a > b) swap(a, b);
        for (int i = 0; i <= 9; ++ i) cout << cnt(b, i) - cnt(a - 1, i) << ' ';
        cout << endl;
    }
    return 0;
}

八、状态压缩DP

91. 最短Hamilton路径
image

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

using namespace std;

const int N=20,M=1<<N;

int f[M][N],w[N][N];//w表示的是无权图

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));//因为要求最小值,所以初始化为无穷大
    f[1][0]=0;//因为零是起点,所以f[1][0]=0;

    for(int i=0;i<1<<n;i++)//i表示所有的情况
     for(int j=0;j<n;j++)//j表示走到哪一个点
      if(i>>j&1)
       for(int k=0;k<n;k++)//k表示走到j这个点之前,以k为终点的最短距离
        if(i>>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;//表示所有点都走过了,且终点是n-1的最短距离
    //位运算的优先级低于'+'-'所以有必要的情况下要打括号
    return 0;
}

九、树形DP

285. 没有上司的舞会
image
image
每个人只有两种状态,则设dp[0][i]dp[0][i]为第i个人不来,他的下属所能获得的最大快乐值;
dp[1][i]dp[1][i]为第i个人来,他的下属所能获得的最大快乐值。

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

using namespace std;
const int N = 6010;

int happy[N];
int h[N],e[N],ne[N],idx;
int f[N][2];
int n;
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!=-1;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()
{
    cin>>n;
    for(int i = 1;i <= n;i++)
    {
        cin>>happy[i];
    }
    memset(h,-1,sizeof h);
    int a,b;
    for(int i = 0;i< n-1;i++) //建树
    {
        cin>>a>>b;
        add(b,a);
        has_father[a] = true;
    }
    int root = 1;
    while(has_father[root]) root++; //找到根结点
    dfs(root);
    
    cout<<max(f[root][0],f[root][1])<<endl; //选根与不选根取较大者
    return 0;
}

十、记忆化搜索

901. 滑雪

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

using namespace std;

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

int dp(int x,int y)
{
    int &v = f[x][y]; //引用给f[x][y]取别名
    if(v!=-1) return v; //发现当前位置已经搜过,则直接返回
    
    v = 1;//初始化,最少步数为1
    for(int i = 0;i < 4;i++)
    {
        int a = x + dx[i];
        int b = y + dy[i];
        if(a>=0 && a<n && b>=0 && b<m && h[x][y] > h[a][b]) //符合要求就尝试搜索 
        {
            v = max(v,dp(a,b) + 1);//切记:此处必须dp进行搜索,不可用f[i][j]
        }
    }
    return v; //搜索完毕一定要返回f[x][y]
}
int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<m;j++)
        {
            cin>>h[i][j];
        }
    }
    
    memset(f,-1,sizeof f); //记忆化搜索,表示未被搜索过
    for(int i = 0;i < n;i++)
    {
        for(int j = 0;j < m;j++)
        {
            res = max(res,dp(i,j)); //切记:此处必须dp进行搜索,不可用f[i][j]
        }
    }
    cout<<res<<endl;
    return 0;
}

十一、整数DP

posted @ 2021-12-29 10:32  安河桥北i  阅读(34)  评论(0)    收藏  举报