题目做法大概就是点分治然后背包
前置知识
点分治
应用场景:
- 求树上距离为k的点对数|是否存在
- 路径为k且有限制条件
总之就是dfs暴力会超时的优化
点分治第一步首先要找到一棵树的重心
然后再根据重心来进行分治
judge i 距离当前根为i的点是否存在
dis i 点i与当前根的距离
点分治模板
题意
给定一棵有 n 个点的树,询问树上距离为 k的点对是否存在。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5,M=1e7+6;
int n, m, u, v, w,Q[N],ans[N];
int root,cnt,S,temp[N],dis[N],max_childrenpart[N],size[N];
bool use[N],judge[M];
// judge i 与当前根距离为i的路径是否存在
// use 表示该点有没有被遍历过
struct node
{
int link, w;
};
vector <node> f[N];
queue <int> q;
void find_root(int x,int fa)// 板子
{
max_childrenpart[x] = 0;
size[x] = 1;
for(auto i:f[x])
{
if(i.link==fa||use[i.link])
continue;
find_root(i.link, x);
size[x] += size[i.link];
max_childrenpart[x] = max(size[i.link],max_childrenpart[x]);
}
max_childrenpart[x] = max(max_childrenpart[x],S-max_childrenpart[x]);
if(max_childrenpart[x]<max_childrenpart[root])
root = x;
}
void get_dis(int x,int fa) //板子
{
temp[++cnt] = dis[x];
for(auto i:f[x])
{
if(use[i.link]||i.link==fa)
continue;
dis[i.link] = dis[x] + i.w;
get_dis(i.link, x);
}
}
void solve(int root)//不同题不同
{
while(!q.empty())
q.pop();
for(auto i:f[root])
{
if(use[i.link])
continue;
cnt = 0; // 所有点距离的计数器
dis[i.link] = i.w; // 第一次要初始化一下
get_dis(i.link,root);//计算距离root节点的距离
for (int j = 1; j <= cnt; j++)
{
for (int k = 1; k <= m;k++)//这里肯定是可以优化的 因为没优化re了(judge爆了
{
if(Q[k]>=temp[j])//询问路径大于 遍历的节点到根的距离说明可能存在
{
if(judge[Q[k]-temp[j]])
{
// 如果非当前子树存在到根距离为Q[k]-dis[j]的点
// 说明答案存在
ans[k] = 1;
}
}
}
}
for (int j = 1; j <= cnt;j++)
{
q.push(temp[j]);
judge[temp[j]] = 1;
}
}
while(!q.empty())
{
judge[q.front()] = 0;// 撤回
q.pop();
}
}
void divided(int x)//板子
{
use[x] = 1;
judge[0] = 1;
solve(x);
for(auto i:f[x])
{
if(use[i.link])
continue;
// 初始化根节点
root = 0;
max_childrenpart=n+1;
//反正一定要大不然可能不会更新,之前max_childrenpart=size[i.link]是错的
S= size[i.link];
find_root(i.link,0);
divided(root);
}
}
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i < n;i++)
{
scanf("%d %d %d", &u, &v, &w);
f[u].emplace_back((node){v, w});
f[v].emplace_back((node){u, w});
}
for (int i = 1; i <= m;i++)
scanf("%d", &Q[i]);
root = 0;
S=max_childrenpart[root] = n;
find_root(1,0);//先找到重心
divided(root);//重心开始第一次分治
for (int i = 1; i <= m;i++)
{
if(ans[i])
printf("AYE
");
else
printf("NAY
");
}
return 0;
}
树形dp
emmm应该是树上背包
选课
之前都是用组合背包来写的(虽说现在都忘光了)
题意:
给一个森林,每个点有点权,选子节点一定要先选父节点,求选m个节点的最大权值
dp[i][j]代表i为根选j个节点的最大权值
因为是个森林,先可以用虚拟节点0连接构成一棵树value[0]=0; 然后求dp[0][m+1];
显然dp[i][1]=value[i],dp[i][j]=max(dp[i][j],dp[i][j-1]+dp[v][k]);v是i的子节点
$jin[1,m+1],kin[0,j-1] ecause j-1-k>=0 $
这状态方程意思就是先把i的子节点预定(j-1),然后再求从子节点选k个的最大值(dp[v][k])
#include<bits/stdc++.h>
using namespace std;
const int N = 305;
int n, m, x, y,value[N],size[N],dp[N][N];
bool use[N];
vector<int> f[N];
void dfs(int x)
{
use[x] = 1;
size[x] = 1;
dp[x][1] = value[x];
for(auto i:f[x])
{
if(i==x||use[i])
continue;
dfs(i);
size[x] += size[i];
for (int j = m+1; j >= 1;j--)
{
// 背包倒序,因为此时还没有更新dp[i][k]就相当于没遍历到i这个子节点(就没有重复加i的权值)
for (int k = 1; k <= j - 1;k++)
dp[x][j] = max(dp[x][j], dp[x][j - k] + dp[i][k]);
}
}
}
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n;i++)
{
scanf("%d %d", &x, &y);
f[x].push_back(i);
f[i].push_back(x);
value[i] = y;
}
dfs(0);
printf("%d", dp[0][m + 1]);
return 0;
}
二叉苹果树
题意:
有一个根1,每个边有边权,选子节点的边之前一定要满足选的边中父节点有边连接,求选m条边的最大权值
就是把点权变成了边权,dp也要变一下,因为一个点下面有多条边,dp[i][1]就不是一个确定的数,需要从dp[i][0]状态转移过来
#include<bits/stdc++.h>
using namespace std;
const int N = 1000;
int n, m, x, y, z,size[N],dp[N][N];
bool use[N];
struct node
{
int link, w;
};
vector<node> f[N];
void solve(int x)
{
size[x] = 1;
use[x] = 1;
for(auto i:f[x])
{
if(i.link==x||use[i.link])
continue;
solve(i.link);
size[x] += size[i.link];
for (int j = m; j >=1;j--)
{
for (int k = 0; k <= j-1;k++)
dp[x][j] = max(dp[i.link][k] + i.w+dp[x][j-k-1], dp[x][j]);
}
}
}
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i < n;i++)
{
scanf("%d %d %d", &x, &y, &z);
f[x].push_back((node){y, z});
f[y].push_back((node){x, z});
}
solve(1);
printf("%d
", dp[1][m]);
return 0;
}
bitset 优化
二进制的思想,虽然不知道为什么会快...二进制运算加的速?
利用bitset可以优化一些状态只有0,1的题
也就是说只牵扯到了能不能买,就是一个“是”和“否”的问题,也就是说这个背包并没有什么权值(只有“可以”和“不可以”)这样才能优化 也就是就是dp的值只有0、1
利用二进制运算的 |可以代替max函数
(bs=bs<< value 代表选择了权值为value这个点)
(a|=b Leftrightarrow 取b或者不取b,如果取b则a=b否则a=a)
bitset <M> dp[N];//自动多一维类似vector
//dp[j][k]=dp[j-1][k-a[i]]等价
//初始化
dp[i].reset();
dp[i][0]=1;
dp[i]<<=a[i];
//代表第a[i]位为1,也就是说选择了一个value为a[i]的点
dp[j] |= (dp[j-1]<<a[i]);
其他具体用法可以看这篇博客
(还看到有的人初始化很神奇,直接bs[i]=1<==>bs[i].reset(), b[i][0]=1,虽说不知道会不会有什么奇怪的问题)
E - Master of Subgraph
正片开始
题意
给你T组数据,每组数据有n个点n-1条边联通,并且每个点都有点权,选一些联通的点使其点权和属于[1,m],如果(i in [1,m] 时有子图权值为i则答案的第i位为1,否则为0(i从1开始))
(0<T<16 quad n in (1,3000) quad min (1,1e5) quad w_iin [0,1e5] )
答案形如 长度为m的 100101 这种字符串 ,保证图是一棵树
选择联通的点,说明选了第一个点,后面的点必须是它的子节点,并且要选择它子节点的子节点必须要先选它子节点的父节点(就是说把选的第一个节点看成根,做树上背包)
根据点分治的特点(要么选根节点要么不选根节点)刚好与题意相同
这里bitset有个小细节,因为w*n会爆内存,而只需要统计m以内的,所以大小开到m就够了(开到1e6居然就超内存了)
#include<bits/stdc++.h>
using namespace std;
const int N = 3e3 + 5,M=1e5+6;
int n, m,T, u, v,w[N];
int root,cnt,S,max_childrenpart[N],size[N];
bool use[N];
vector <int> f[N];
bitset<M> ans, dp[N];//dp i 以i为根所选的节点编号
void find_root(int x,int fa)// 板子
{
max_childrenpart[x] = 0;
size[x] = 1;
for(auto i:f[x])
{
if(i==fa||use[i])
continue;
find_root(i, x);
size[x] += size[i];
max_childrenpart[x] = max(size[i],max_childrenpart[x]);
}
max_childrenpart[x] = max(max_childrenpart[x],S-max_childrenpart[x]);
if(max_childrenpart[x]<max_childrenpart[root])
root = x;
}
void solve(int x,int fa)
{
dp[x] <<= w[x];//选择这个节点
for(auto i:f[x])
{
if(use[i]||i==fa)
continue;
dp[i] = dp[x]; // 初始化一下
solve(i,x);
dp[x] |= dp[i];
}
}
void divided(int x)//板子
{
use[x] = 1;
dp[x].reset();// 全初始化为0
dp[x][0] = 1;//初始化第0位为1
solve(x,0);
ans |= dp[x];// ans'加上'这个权值
for(auto i:f[x])
{
if(use[i])
continue;
root = 0;
max_childrenpart[root] = n + 1;
S = size[i];//初始化
find_root(i,0);
divided(root);
}
}
int main()
{
scanf("%d", &T);
while(T--)
{
ans.reset();
scanf("%d %d", &n, &m);
for (int i = 0; i <= n;i++)
{
f[i].clear();
use[i] = 0;
max_childrenpart[i] = 0;
}
for (int i = 1; i < n; i++)
{
scanf("%d %d", &u, &v);
f[u].emplace_back(v);
f[v].emplace_back(u);
}
for (int i = 1; i <= n;i++)
scanf("%d", &w[i]);
root = 0;
S=max_childrenpart[root] = n;
find_root(1,0);//先找到重心
divided(root);//重心开始第一次分治
for (int i = 1; i <= m;i++)
printf("%d", (int) ans[i]);
printf("
");
}
return 0;
}
/*
2
4 10
1 2
2 3
3 4
3 2 7 5
6 10
1 2
1 3
2 5
3 4
3 6
1 3 5 7 9 11
*/