蓝桥杯 第九讲 复杂DP
1050. 鸣人的影分身(整数划分问题)
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. 糖果
#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)
相当于找一个最长回文子序列
#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)
#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. 包子凑数(完全背包+数论)
二维空间朴素写法
#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)
#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
#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[] 如下
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)