【集训】树上 & 状压DP
P1352 没有上司的舞会
树形 DP
设
转移:
DFS一遍求解即可。
#include <bits/stdc++.h>
using namespace std;
int dp[100010][2],a[100010];
vector<int> G[100010];
void dfs(int rt,int fa){
dp[rt][0]=0;
dp[rt][1]=a[rt];
for(int i=0;i<G[rt].size();i++){
int to=G[rt][i];
if(to==fa) continue;
dfs(to,rt);
dp[rt][1]=dp[rt][1]+max(0,dp[to][0]);
dp[rt][0]=dp[rt][0]+max(0,max(dp[to][0],dp[to][1]));
}
}
int main(){
int n,u,v;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n-1;i++){
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1,0);
int ans=max(ans,max(dp[1][0],dp[1][1]));
cout<<ans<<endl;
return 0;
}
P2014 [CTSC1997] 选课
树形 DP
设
初值:
转移:从下往上dp,
因为
答案:
时间复杂度
#include<bits/stdc++.h>
using namespace std;
const int N=305,M=2001;
int f[N][N],siz=0,n,m;
int head[M],nxt[M],son[M];
void add(int x,int y){
siz++;
nxt[siz]=head[x];
head[x]=siz;
son[siz]=y;
}
void dp(int x){
for (int e=head[x];e;e=nxt[e]){
int y=son[e];
dp(y);
for (int i=m+1;i>1;i--)
for (int j=i-1;j>0;j--)
f[x][i]=max(f[x][i],f[x][i-j]+f[y][j]);
}
}
int main(){
cin>>n>>m;
for (int i=1;i<=n;i++){
int a,tmp;
cin>>a>>tmp;
f[i][1]=tmp;
add(a,i);
}
dp(0);
cout<<f[0][m+1]<<endl;
return 0;
}
P2607 [ZJOI2008] 骑士
基环树 DP
每个点向自己讨厌的人反向连边,形成基环内向树。
找到环,枚举环上的每一个点,并断开,于是转化成了 没有上司的舞会。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1000010, INF = 1e9;
int n;
int h[N], e[N], rm[N], w[N], ne[N], idx;
LL f1[N][2], f2[N][2];
bool st[N], ins[N];
LL ans;
inline void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs_f(int u, int ap, LL f[][2]){
for (int i = h[u]; ~i; i = ne[i]){
if (rm[i]) continue;
int j = e[i];
dfs_f(j, ap, f);
f[u][0] += max(f[j][0], f[j][1]);
}
f[u][1] = -INF;
if (u != ap){
f[u][1] = w[u];
for (int i = h[u]; ~i; i = ne[i]){
if (rm[i]) continue;
int j = e[i];
f[u][1] += f[j][0];
}
}
}
void dfs_c(int u, int from){
st[u] = ins[u] = true;
for (int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if (!st[j]) dfs_c(j, i);
else if (ins[j]){
rm[i] = 1;
dfs_f(j, -1, f1);
dfs_f(j, u, f2);
ans += max(f1[j][0], f2[j][1]);
}
}
ins[u] = false;
}
int main(){
cin >> n;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i ++ ){
int a, b;
cin >> a >> b;
add(b, i);
w[i] = a;
}
for (int i = 1; i <= n; i ++ )
if (!st[i])
dfs_c(i, -1);
cout << ans << endl;
return 0;
}
P3177 [HAOI2015] 树上染色
树形 DP
设
直接算答案不好算,考虑把答案拆分到边权上
枚举每一条边,发现对答案的贡献为:
DFS 一遍求解即可
#include<bits/stdc++.h>
#define ll long long
const int N=2005;
using namespace std;
struct ahaha{
int w,to,nxt;
}e[N<<1];int tot,h[N];
inline void add(int u,int v,int w){
e[tot].w=w,e[tot].to=v,e[tot].nxt=h[u];h[u]=tot++;
}
int n,m,sz[N];
ll f[N][N];
void dfs(int u,int fa){
sz[u]=1;f[u][0]=f[u][1]=0;
for(int i=h[u];~i;i=e[i].nxt){
int v=e[i].to;
if(v==fa) continue;
dfs(v,u);
sz[u]+=sz[v];
for(int j=min(m,sz[u]);j>=0;--j){
if(f[u][j]!=-1)
f[u][j]+=f[v][0]+(ll)sz[v]*(n-m-sz[v])*e[i].w;
for(int k=min(j,sz[v]);k;--k){
if(f[u][j-k]==-1)continue;
ll val=(ll)(k*(m-k)+(sz[v]-k)*(n-m-sz[v]+k))*e[i].w;
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]+val);
}
}
}
}
int main(){
memset(h,-1,sizeof h);
cin>>n>>m;
if(n-m<m)m=n-m;
for(int i=1;i<n;++i){
int u,v,w;
cin>>u>>v>>w;
add(u,v,w),add(v,u,w);
}
memset(f,-1,sizeof f);
dfs(1,-1);
cout<<f[1][m]<<endl;
return 0;
}
P1272 重建道路
考虑设
转移:
DFS 求解
P3478 [POI2008] STA-Station
考虑假设以
那么当根节点为
然后 转移就可以了
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=2000005;
int n,d[N],sz[N],f[N];
int h[N],e[N],ne[N],idx;
int mx=-1e9,ans;
void add(int u,int v){
e[++idx]=v,ne[idx]=h[u],h[u]=idx;
}
void dfs(int u,int fa){
sz[u]=1,d[u]=d[fa]+1;
for(int i=h[u];i;i=ne[i]){
int j=e[i];
if(j==fa) continue;
dfs(j,u);
sz[u]+=sz[j];
}
}
void dfs2(int u,int fa){
for(int i=h[u];i;i=ne[i]){
int j=e[i];
if(j==fa) continue;
f[j]=f[u]-sz[j]+n-sz[j];
dfs2(j,u);
}
}
signed main(){
cin>>n;
for(int i=1;i<=n-1;i++){
int u,v;
cin>>u>>v;
add(u,v),add(v,u);
}
dfs(1,0);
for(int i=1;i<=n;i++) f[1]+=d[i];
dfs2(1,0);
for(int i=1;i<=n;i++){
if(f[i]>mx){
mx=f[i],ans=i;
}
}
cout<<ans<<endl;
return 0;
}
P4365 [九省联考 2018] 秘密袭击 coat
P3780 [SDOI2017] 苹果树
P1879 [USACO06NOV] Corn Fields G
状压dp板子
考虑 f_{i,S}$ 表示前
转移:
然后也可以滚动数组优化。
#include <bits/stdc++.h>
using namespace std;
const int N=13,M=1<<12,P=1e8;
int f[N][M],F[N],g[M],a[N][N];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
F[i]=(F[i]<<1)+a[i][j];
for(int i=0;i<(1<<m);i++)
if(!(i&(i>>1))&&!(i&(i<<1))){
g[i]=1;
if((i&F[1])==i) f[1][i]=1;
}
for(int x=2;x<=n;x++)
for(int j=0;j<(1<<m);j++)
if(((j&F[x-1])==j)&&g[j])
for(int k=0;k<(1<<m);k++)
if(((k&F[x])==k)&&!(j&k)&&g[k])
f[x][k]=(f[x][k]+f[x-1][j])%P;
int ans=0;
for(int i=0;i<(1<<m);i++)
ans=(ans+f[n][i])%P;
cout<<ans<<endl;
return 0;
}
P1896 [SCOI2005] 互不侵犯
设
DFS 预处理出所有合法的状态,然后暴力转移即可
答案就是
#include <bits/stdc++.h>
using namespace std;
const int M = 10;
const int C = 2000;
int s[C], g[C], cnt = 0, n, y;
long long f[M][C][100];
void dfs(int h, int m, int d) {
if (d >= n) {
s[++cnt] = h;
g[cnt] = m;
return;
}
dfs(h, m, d + 1);
dfs(h + (1 << d), m + 1, d + 2);
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> y;
dfs(0, 0, 0);
for (int i = 1; i <= cnt; i++) {
f[1][i][g[i]] = 1;
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= cnt; j++) {
for (int k = 1; k <= cnt; k++) {
if (s[j] & s[k]) continue;
if ((s[j] << 1) & s[k]) continue;
if (s[j] & (s[k] << 1)) continue;
for (int t = y; t >= g[j]; t--) {
f[i][j][t] += f[i - 1][k][t - g[j]];
}
}
}
}
long long a = 0;
for (int i = 1; i <= cnt; i++) {
a += f[n][i][y];
}
cout << a << "\n";
return 0;
}
P3959 [NOIP2017 提高组] 宝藏
状态DP,下文中i是一个
状态f[i][j]表示:
- 集合:所有包含i中所有点,且树的高度等于j的生成树
- 属性:最小花费
- 状态计算:枚举i的所有非全集子集S作为前j - 1层的点,剩余点作为第j层的点。
- 核心: 求出第j层的所有点到S的最短边,将这些边权和乘以j,直接加到f[S][j - 1]上,即可求出f[i][j]。
时间复杂度
包含
对于每个子集需要
因此总时间复杂度是
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << N, INF = 0x3f3f3f3f;
int n, m;
int d[N][N], g[N][M];
int f[M][N];
int main()
{
scanf("%d%d", &n, &m);
memset(d, 0x3f, sizeof d);
for (int i = 0; i < n; i ++ ) d[i][i] = 0;
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
a --, b -- ;
d[a][b] = d[b][a] = min(d[a][b], c);
}
memset(g, 0x3f, sizeof g);
for (int i = 0; i < n; i ++ )
for (int j = 0; j < 1 << n; j ++ )
for (int k = 0; k < n; k ++ )
if (j >> k & 1)
g[i][j] = min(g[i][j], d[i][k]);
memset(f, 0x3f, sizeof f);
for (int i = 0; i < n; i ++ ) f[1 << i][0] = 0;
for (int i = 1; i < 1 << n; i ++ )
for (int j = i - 1 & i; j; j = j - 1 & i) // 枚举i的子集
{
int r = i ^ j, cost = 0;
for (int k = 0; k < n; k ++ )
if (j >> k & 1)
{
cost += g[k][r];
if (cost >= INF) break;
}
if (cost >= INF) continue;
for (int k = 1; k < n; k ++ )
f[i][k] = min(f[i][k], f[r][k - 1] + cost * k);
}
int res = INF;
for (int i = 0; i < n; i ++ )
res = min(res, f[(1 << n) - 1][i]);
printf("%d\n", res);
return 0;
}
P8189 [USACO22FEB] Redistributing Gifts G
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 19, M = 1 << 18; // N为最大点数,M为状态集合的总数(2^n)
int n, Q, s[N]; // n为点的数量,Q为查询次数,s[i]表示点i可以到达的点的集合(用二进制表示)
ll dp[M][N], g[M], h[M]; // dp[i][j]表示从集合i的最小点出发到达点j的方案数,g[i]表示集合i连成一个置换环的方案数,h[i]表示集合i分成若干个置换环的方案数
char c[N]; // 用于读取查询的字符串
int main(){
cin >> n; // 输入点的数量n
// 输入每个点的转移关系,构建s数组
for (int i = 0; i < n; i++){
bool flag = 0; // 标记是否已经找到了一个自环(即点i可以转移到自己)
for (int j = 0; j < n; j++){
int x;
cin >> x; // 输入点i的下一个转移点x(从1开始编号,减1后从0开始)
x--;
if (!flag)
s[i] |= (1 << x); // 将点x加入点i可以到达的集合s[i]中
if (x == i) // 如果x等于i,说明存在自环
flag = 1;
}
}
// 初始化dp数组,每个单独的点i,从i出发到i的方案数为1
for (int i = 0; i < n; i++)
dp[1 << i][i] = 1;
// 动态规划求解dp数组
for (int i = 1; i < (1 << n); i++){ // 枚举所有状态集合i(从1到2^n-1)
int p = __lg(i & (-i)); // 找到集合i中最小的点p(即最低位的1)
for (int j = p; j < n; j++) // 枚举当前集合i的终点j
if (dp[i][j]) // 如果从集合i的最小点出发可以到达点j
for (int k = p + 1; k < n; k++) // 枚举下一个点k
if (!(i & (1 << k)) && s[j] & (1 << k)) // 如果点k不在集合i中且点j可以转移到点k
dp[i | (1 << k)][k] += dp[i][j]; // 更新dp值,将点k加入集合i并转移到点k
}
// 计算每个集合i连成一个置换环的方案数g[i]
for (int i = 1; i < (1 << n); i++) // 枚举所有状态集合i
for (int j = 0; j < n; j++){ // 枚举集合i的最后一个点j
int p = __lg(i & (-i)); // 找到集合i中最小的点p
if (s[j] & (1 << p)) // 如果点j可以回到集合i的最小点p
g[i] += dp[i][j]; // 更新g[i],将从点j回到点p的方案数加入g[i]
}
// 计算将集合i分成若干个置换环的方案数h[i]
h[0] = 1; // 空集的方案数为1
for (int i = 1; i < (1 << n); i++) // 枚举所有状态集合i
for (int j = i; j; j = (j - 1) & i) // 枚举集合i的所有子集j
if (j & (i & (-i))) // 如果子集j包含集合i的最小点
h[i] += g[j] * h[i ^ j]; // 更新h[i],将子集j连成一个置换环的方案数与剩余部分的方案数相乘
// 处理查询
cin >> Q; // 输入查询次数Q
while (Q--){
cin >> c; // 输入查询的字符串c
int ans = 0; // 初始化查询结果
for (int i = 0; i < n; i++)
if (c[i] == 'H') // 如果字符为'H',表示点i在查询集合中
ans |= (1 << i); // 将点i加入查询集合
// 输出查询结果,将查询集合的方案数与剩余集合的方案数相乘
cout << h[ans] * h[((1 << n) - 1) ^ ans] << "\n";
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!