蓝桥杯 第九讲 复杂DP

1050. 鸣人的影分身(整数划分问题)

image
image

DP解法

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

using namespace std;

const int N = 11;

int f[N][N]; //f[i][j]表示:总和为i,当前选取了j个数的选法个数

int n,m,T;

int main()
{
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d%d", &m, &n);
        memset(f,0,sizeof f);
        f[0][0] = 1;  //总和为0,选0个数的方案只有1种
        for (int i = 0; i <= m; i ++ )
        {
            for(int j = 1;j<=n;j++)
            {
                f[i][j] = f[i][j-1];
                if(i >= j) f[i][j] += f[i-j][j]; //当且i不小于j时可以加
            }
        }
        cout<<f[m][n]<<endl;
    }
    return 0;
}

DFS暴力枚举

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

using namespace std;

const int N = 11;
int T,n,m;
int ans;
void dfs(int u,int num,int k)  //枚举第u个分身,当前剩余num个能量,消耗k个能量
{
    if(u >= n) //递归出口
    {
        if(num == 0) ++ans;  //没有能量,说明是一种方案
        return;
    }
    if(k > num) return;  //合理剪枝
    for (int i = k; i <= num; i ++ ) //从k到num开始枚举
    {
        dfs(u+1,num-i,i);
    }
    return;
}

int main()
{
    scanf("%d", &T);
    while(T--)
    {
        ans = 0;
        scanf("%d%d", &m, &n);
        dfs(0,m,0); //分身为0~n-1号,剩余m能量,0消耗开始枚举
        cout<< ans << endl;
    }
    return 0;
}

1047. 糖果

image

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

using namespace std;

const int N = 110;

int f[N][N];  //f[i][j]表示:前i个公司中,选出的糖果总数对k取模结果为j的糖果数量的最大值
int n,k;
int w[N]; 
int main()
{
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%d", &w[i]);
    }
    memset(f,-0x3f,sizeof f);  //由于要求最大值,故将所有不合法的情况的值设为负无穷
    f[0][0] = 0; //0个公司中选0个最大值为0
    for (int i = 1; i <= n; i ++ )
    {
        for(int j = 0;j < k;j++)
        {
            f[i][j] = max(f[i-1][j],f[i-1][(j - w[i] % k + k) % k] + w[i]);  //01背包思想
            //注意转移方程中的(j−w[i])%k)可能为负的
            //必须要将余数变成[0,n−1]之间,所以cpp负数取余要变成(j−w[i])%k+k)%k。
        }
    }
    int ans = -1;
    cout << f[n][0] << endl;
    return 0;
}

1222. 密码脱落(区间DP)

image
image
相当于找一个最长回文子序列

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

using namespace std;

const int N = 1010;

string s;
int f[N][N];
int main()
{
    cin>>s;
    int n = s.size();
    for(int len = 1;len<=n;len++)
    {
        for(int l = 0;l + len - 1 < n;l++) //先枚举长度,否则会导致使用未计算的状态而出错
        {
            int r = l + len - 1;
            if(len == 1) f[l][r] = 1; //一个字母即可构成回文序列
            else
            {
                if(s[l] == s[r]) f[l][r] = f[l+1][r-1] + 2;  //包含[l,r]
                f[l][r] = max(f[l][r],f[l][r-1]);  //包含l
                f[l][r] = max(f[l][r],f[l+1][r]);  //包含r
                //f[l][r] = max(f[l][r],f[l+1][r-1]); 此类已被f[l][r-1]和f[l+1][r]包含
            }
        }
    }
    cout<<n - f[0][n-1]<<endl;  //求的是最长回文序列,题目要的是增加ans个字母构成回文串
    //因此,只需给落单的字母配对即可
    return 0;
}

1220. 生命之树(树形DP)

image

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

using namespace std;
typedef long long LL;

const int N = 1e5 + 10,M = 2*N;

LL f[N];
int w[N]; //w[i]表示以i为root的包含i的所有连通块的权值之和的最大值Max
int h[M],ne[M],e[M],idx;

void add(int a,int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}
int n;

void dfs(int u,int dad) //树形DP:求权值之和最大的连通块
{
    f[u] = w[u];   //权值之和,包含u
    for(int i = h[u];i!=-1;i=ne[i]) //遍历其每一条边
    {
        int j = e[i];
        if(j!=dad) //避免向上重复搜索
        {
            dfs(j,u);
            f[u] += max(0ll,f[j]); //小于0的不要
        }
    }
}
int main()
{
    memset(h, -1, sizeof h);
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%d", &w[i]);
    }
    if(n == 1)
    {
        cout << max(w[1],0)<<endl;
        return 0;
    }
    int a,b;
    for (int i = 0; i < n-1; i ++ )
    {
        scanf("%d%d", &a, &b);
        add(a,b);
        add(b,a);
    }
    dfs(1,-1);
    LL ans = 0; //注意:当树上所有权值为0时,可以一个也不选,答案为0
    for(int i = 1;i<=n;i++)  //求每个点为root的连通块的权值最大值
    {
        ans = max(ans,f[i]);
    }
    cout<<ans<<endl;
    return 0;
}

1226. 包子凑数(完全背包+数论)

image
image

二维空间朴素写法

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

using namespace std;

const int N = 10010; //物品体积之和不超过10000
int n,a[110];
bool f[110][N]; //f[i][j]:前i个物品中,总体积为j的选法是否可行

int gcd(int a,int b)
{
    return b==0?a:gcd(b,a%b);
}

int main()
{
    scanf("%d", &n);
    int d = 0;
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%d", &a[i]);
        d = gcd(d,a[i]); //求n个数的最大公约数
    }
    if(d != 1)  //最大公约数不为1,则有无数个凑不出的数
    {
        puts("INF");
        return 0;
    }
    f[0][0] = 1;  //初始化:0个物品可以凑出体积为0的
    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 0; j < N; j ++ )
        {
            f[i][j] = f[i-1][j]; //不选第i个物品
            if(a[i]<=j) f[i][j] |= f[i][j-a[i]]; //选第i个物品,这里进行了优化
        }
    }
    int ans = 0;
    for (int i = 0; i < N; i ++ ) //统计不可行的方案
    {
        if(!f[n][i]) ++ans;
    }
    cout << ans << endl;
    return 0;
}

优化为一维

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

using namespace std;

const int N = 10010; //物品体积之和不超过10000
int n,a[110];
bool f[N]; //f[i][j]:前i个物品中,总体积为j的选法是否可行

int gcd(int a,int b)
{
    return b==0?a:gcd(b,a%b);
}

int main()
{
    scanf("%d", &n);
    int d = 0;
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%d", &a[i]);
        d = gcd(d,a[i]); //求n个数的最大公约数
    }
    if(d != 1)  //最大公约数不为1,则有无数个凑不出的数
    {
        puts("INF");
        return 0;
    }
    f[0] = 1;  //初始化:0个物品可以凑出体积为0的
    for (int i = 1; i <= n; i ++ )
    {
        for (int j = a[i]; j < N; j ++ )
        {
            f[j] |= f[j-a[i]]; //选第i个物品,这里进行了优化
        }
    }
    int ans = 0;
    for (int i = 0; i < N; i ++ ) //统计不可行的方案
    {
        if(!f[i]) ++ans;
    }
    cout << ans << endl;
    return 0;
}

1070. 括号配对(区间dp)

image

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

using namespace std;

const int N = 110,INF = 0x3f3f3f3f;

string s;
int f[N][N]; //f[l][r]:添加字符使其成为BRE的最小个数

bool ismatch(int l,int r)
{
     if((s[l] == '(' && s[r] == ')')||(s[l] == '['&& s[r] == ']')) return true;
     else return false;
}
int main()
{
    cin>>s;
    int n = s.size();
    for(int len = 1;len<=n;len++)
    {
        for(int l = 0;l + len -1<n;l++)
        {
            int r = l + len - 1;
            if(len == 1) //长度为1,一定需要补一个字符
            {
                f[l][r] = 1;
                continue;
            }
            f[l][r] = INF;
            if(ismatch(l,r)) //匹配
            {
                f[l][r] = f[l+1][r-1];
            }
            
            for(int k = l;k<r;k++) //不匹配
            {
                f[l][r] = min(f[l][r],f[l][k] + f[k+1][r]);
            }
        }
    }
    cout<<f[0][n-1]<<endl;
    return 0;
}

1078. 旅游规划

回忆:求树的直径,两次BFS
image
image

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

using namespace std;

const int N = 2e5 + 20,M = 2*N;

int h[N],e[M],ne[M],idx;
int n,m,a,b;

int d1[N],d2[N];//该结点所在路径长度的最大值和次大值
int p1[N],up[N];//该结点的最长路径的去向
int Max=-1; //最长路径的长度
void add(int a,int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}

void dfs_d(int u,int father)
{
    for(int i = h[u];i!=-1;i=ne[i])
    {
        int j = e[i];
        if(j == father) continue;
        dfs_d(j,u);
        int dist = d1[j] + 1;//从j往下走的最大值
        if(dist > d1[u])
        {
            d2[u] = d1[u];//最大值给次大值
            d1[u] = dist;//dist给最大值
            p1[u] = j; //从j去的最长路径
        }
        else if(dist > d2[u]) //比次大值大就更新次大值
        {
            d2[u] = dist;
        }
    }
    Max = max(Max,d1[u]+d2[u]);//更新最长路径的长度
}
void dfs_u(int u,int father)
{
    for(int i = h[u];i!=-1;i=ne[i])
    {
        int j = e[i];
        if(j == father) continue;
        up[j] = up[u] + 1;
        if(j == p1[u]) up[j] = max(up[j],d2[u] + 1);//只能用次大值更新
        else up[j] = max(up[j],d1[u] + 1);//否则用最大值更新
        dfs_u(j,u);
    }
}
int main()
{
    memset(h,-1,sizeof h);
    scanf("%d", &n);
    for (int i = 0; i < n - 1; i ++ )
    {
        scanf("%d%d", &a, &b);
        add(a,b);
        add(b,a);
    }
    dfs_d(0,-1);
    dfs_u(0,-1);
    
    for (int i = 0; i < n; i ++ )
    {
        int d[3] = {d1[i],d2[i],up[i]};
        sort(d,d+3); //排序
        if(d[1]+d[2] == Max) cout<<i<<endl; //选较大的二者求和,若为最大长度,则点i在最长路径上
    }
    return 0;
}

1217. 垒骰子

dp + 矩阵乘法
一个骰子的摆放方式是24种 (任意一面朝上侧面都可以旋转四次 4 * 6 = 24)
f[i][j] 表示前i个骰子且第i个骰子数字j朝上
状态转移 f[i][j] = f[i - 1][k] * 4 (侧面可旋转四次) (k从1到6 前i-1个骰子数字k朝上且k不与j的对面互斥)
矩阵乘法
Fn[] = {f[n][1], f[n][2], f[n][3], f[n][4], f[n][5], f[n][6]}
Fn+1[] = {f[n+1][1], f[n+1][2], f[n+1][3], f[n+1][4], f[n+1][5], f[n+1][6]}
目的是找到一个矩阵A 使得Fn+1[] = A * Fn[] 如下
image

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

using namespace std;

const int N = 6,mod = 1e9 + 7;
typedef long long LL;

int a[N][N]; //记录对立关系,对立则0,不对立是1
int n,m,x,y;
int f[N][N]; //f[i][j]:第i个骰子里,最上面点数为j的方案数
int get_op(int x)
{
    if(x>=3) return x-3;
    return x+3;
}
void mul(int c[][N],int a[][N],int b[][N])
{
    static int t[N][N];
    memset(t,0,sizeof t);
    for(int i=0;i<N;i++)
    {
        for(int j=0;j<N;j++)
        {
            for(int k=0;k<N;k++)
            {
                t[i][j] = (t[i][j] + (LL)a[i][k] * b[k][j]) % mod;
            }
        }
    }
    memcpy(c,t,sizeof t);
}
int main()
{
    scanf("%d%d", &n, &m);
    for(int i=0;i<N;i++)
    {
        for(int j=0;j<N;j++)
        {
            a[i][j] = 4;
        }
    }
    while (m -- )
    {
        scanf("%d%d", &x, &y);
        --x,--y;
        a[x][get_op(y)] = 0;
        a[y][get_op(x)] = 0;
    }
    for(int i=0;i<N;i++) f[0][i] = 4;//第1个骰子,每个点数在顶端都有4种情况(旋转)
    for(int k=n-1;k;k>>=1) //矩阵快速幂
    {
        if(k&1==1) mul(f,f,a); //F = F * A
        mul(a,a,a); //A = A * A
    }
    int ans = 0;
    for(int i=0;i<N;i++) ans = (ans + f[0][i]) % mod;
    cout<<ans<<endl;
    return 0;
}
posted @   安河桥北i  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示