[HDU6566]The Hanged Man
话说这一套题的题目好像都是以塔罗牌为名字的啊......而且这个题目和倒吊人有什么关系?
壹、题目描述
贰、题解
首先想到一个比较朴素的树 \(\tt DP\),设 \(f_{i,j,0|1}\) 表示考虑到树上第 \(i\) 个点,背包容量为 \(j\),不选/选择这个点的最大价值,显然,状态合并的时候是一个 \(\max\) 卷积,在目前的科技上讲,任何函数的 \(\min/\max\) 卷积都是只能暴力做的,所以这样的做法无法进行更多的优化。
考虑我们暴力做卷积是 \(m^2\) 的,但是单独加入一个数字只有 \(m\) 的复杂度,能否让我们考虑问题的方式变换一下?
这道题从一个玄学的方向入手——按照 \(\rm dfs\) 序加入点,但是考虑到当 \(\rm dfs\) 序加一之后,可能从一个子树跑到另外一个子树去了,即这种情况
在这个过程中,这个点向上爬了很多步,这会造成一个什么结果?我们需要将这个点以及其所有祖先记录下来,这样我们最多需要记录 \(n\times m\times 2^n\) 的状态,如果滚动掉一维,也会有 \(m\times 2^n\) 的数组,这是我们无法接受的。
考虑如何才能降低它祖先的个数——点分治,我们考虑建出点分树,在这个点分树上,一个点和在它原树上相邻的点只有可能是这几种情况:
- 和它相邻的点是其祖先;
- 和它相邻的点在其子树内;
在点分树上的兄弟在原树上不可能相邻,同时我们只需要考虑每个点的祖先即可将这两种情况都考虑到,如果暴力记录所有的祖先是否选择的情况,由于点分树深度是 \(\log n\),那么这部分状态数就是 \(2^{\log n}=n\),但是我们还需要背包容量的一维,以及 \(n\) 的一维,所以最后的空间就是 \(\mathcal O(nm\times n=n^2m)\),但是注意到我们可以滚动掉 \(n\),所以空间 \(\mathcal O(nm)\),然后我们暴力做 \(01\) 背包即可。
对于背包的细节,假设我们当前访问到 \(u\),那么我们就枚举其祖先选择的所有状态 \(s\),对于可以将 \(u\) 选择的状态,我们进行更新,代码如下:
其中 \(\tt gra[u][v]\) 表示在原树上是否连通,\(\tt sta[i]\) 是我们将其所有祖先按照深度编号拍到链上。
for(int s=0; s<(1<<dep); ++s){
int flg=0;
for(int i=0; i<dep; ++i)
if(((s>>i)&1) && gra[sta[i]][u]){
flg=1; break;
}
if(flg) continue; // cannot choose this node
int to=s^(1<<dep);
for(int j=a[u]; j<=m; ++j)
if(f[s][j-a[u]].cnt){
f[to][j].merge(f[s][j-a[u]]+b[u]);
}
}
然后,我们递归其子树,递归完子树之后,我们要将 \(u\) 在二进制中的这一维删掉,从 \(\tt s|(1<<dep)\) 向 \(\tt s\) 转移,这些可以分析一下代码。
注意方案数必须开 \(\tt longlong\),因为菊花图嘿嘿嘿,若所有花瓣价值一样,那么除了根以外我们可以选择 \(2^{n-1}\) 左右......
叁、参考代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
template<class T>inline T fab(const T x){return x<0? -x: x;}
template<class T>inline T readin(T x){
x=0; int f=0; char c;
while((c=getchar())<'0' || '9'<c) if(c=='-') f=1;
for(x=(c^48); '0'<=(c=getchar()) && c<='9'; x=(x<<1)+(x<<3)+(c^48));
return f? -x: x;
}
const int maxn=50;
const int inf=0x3f3f3f3f;
const int maxm=5000;
const int maxs=1<<6;
int n, m;
int a[maxn+5], b[maxn+5];
int gra[maxn+5][maxn+5];
namespace BITREE{
int root;
struct edge{
int to, nxt;
edge(){}
edge(const int T, const int N): to(T), nxt(N){}
}e[maxn*2+5];
int tail[maxn+5], ecnt;
inline void clear(){
ecnt=0;
memset(tail+1, -1, sizeof(tail[0])*n);
}
inline void add_edge(const int u, const int v){
e[ecnt]=edge(v, tail[u]); tail[u]=ecnt++;
e[ecnt]=edge(u, tail[v]); tail[v]=ecnt++;
}
struct node{
int val;
ll cnt;
node(){}
node(const int V, const ll C): val(V), cnt(C){}
inline void merge(const node rhs){
if(rhs.val>val) val=rhs.val, cnt=0;
if(val==rhs.val) cnt+=rhs.cnt;
}
inline node operator +(const int rhs){
return node(val+rhs, cnt);
}
inline void operator =(const int rhs){
val=rhs, cnt=1;
}
};
node f[maxs+5][maxm+5];
// save the chain
int sta[maxn+5];
void dfs(const int u, const int par, const int dep){
// if the current node is root
if(dep==0){
// initial, pay attention to f[0][0].cnt=1, picking nothing is also a solution
f[0][0]=0;
f[1][a[u]]=b[u];
}
else{
// enumerate each situation
for(int s=0; s<(1<<dep); ++s){
int flg=0;
for(int i=0; i<dep; ++i)
if(((s>>i)&1) && gra[sta[i]][u]){
flg=1; break;
}
if(flg) continue; // cannot choose this node
int to=s^(1<<dep);
for(int j=a[u]; j<=m; ++j)
if(f[s][j-a[u]].cnt){
f[to][j].merge(f[s][j-a[u]]+b[u]);
}
}
}
sta[dep]=u;
for(int i=tail[u], v; ~i; i=e[i].nxt)
if((v=e[i].to)!=par){
dfs(v, u, dep+1);
// upload the infomation
for(int s=0; s<(1<<(dep+1)); ++s){
int flg=0;
for(int j=0; j<=dep; ++j)
if(((s>>j)&1) && gra[sta[j]][v]){
flg=1; break;
}
if(flg) continue;
int from=s^(1<<(dep+1));
for(int j=0; j<=m; ++j){
f[s][j].merge(f[from][j]);
f[from][j]=node(0,0); // pay attention to clear
}
}
}
}
void launch(){
dfs(root, 0, 0);
for(int j=1; j<=m; ++j)
f[0][j].merge(f[1][j]);
printf("%lld", f[0][1].cnt);
f[0][1]=f[1][1]=node(0, 0); // clear the array
for(int j=2; j<=m; ++j){
printf(" %lld", f[0][j].cnt);
f[0][j]=f[1][j]=node(0, 0); // clear the array
}
putchar('\n');
}
}
namespace ORIGIN_GRAPH{
struct edge{
int to, nxt;
edge(){}
edge(const int T, const int N): to(T), nxt(N){}
}e[maxn*2+5];
int tail[maxn+5], ecnt;
inline void add_edge(const int u, const int v){
e[ecnt]=edge(v, tail[u]); tail[u]=ecnt++;
e[ecnt]=edge(u, tail[v]); tail[v]=ecnt++;
}
int siz[maxn+5], f[maxn+5];
// whether the node has been removed
int del[maxn+5];
// find the root
void dfs(const int u, const int par, const int n, int& root){
siz[u]=1, f[u]=0;
for(int i=tail[u], v; ~i; i=e[i].nxt)
if((v=e[i].to)!=par && !del[v]){
dfs(v, u, n, root);
siz[u]+=siz[v], f[u]=max(f[u], siz[v]);
}
f[u]=max(f[u], n-siz[u]);
if(f[u]<f[root]) root=u;
return;
}
void makert(const int u, const int par){
siz[u]=1;
for(int i=tail[u], v; ~i; i=e[i].nxt)
if((v=e[i].to)!=par && !del[v])
makert(v, u), siz[u]+=siz[v];
}
int buildtre(const int, const int);
void divide(const int rt){
del[rt]=1; makert(rt, 0);
for(int i=tail[rt], v; ~i; i=e[i].nxt) if(!del[v=e[i].to])
BITREE::add_edge(rt, buildtre(v, siz[v]));
}
int buildtre(const int u, const int n){
int root=0; f[0]=inf;
dfs(u, 0, n, root);
divide(root);
return root;
}
inline void clear(){
ecnt=0;
memset(tail+1, -1, sizeof(tail[0])*n);
memset(del+1, 0, sizeof(del[0])*n);
}
}
inline void input(){
n=readin(1), m=readin(1);
for(int i=1; i<=n; ++i)
for(int j=1; j<=n; ++j)
gra[i][j]=0;
ORIGIN_GRAPH::clear();
for(int i=1; i<=n; ++i)
a[i]=readin(1), b[i]=readin(1);
int u, v;
for(int i=1; i<n; ++i){
u=readin(1), v=readin(1);
gra[u][v]=gra[v][u]=1;
ORIGIN_GRAPH::add_edge(u, v);
}
}
signed main(){
int T=readin(1);
for(int t=1; t<=T; ++t){
input();
BITREE::clear();
BITREE::root=ORIGIN_GRAPH::buildtre(1, n);
printf("Case %d:\n", t);
BITREE::launch();
}
return 0;
}
多组没有清空 \(\tt f[0][i]\) 和 \(\tt f[1][i]\),以及 \(\tt del[i]\).
并且还要注意从 \(v\rightarrow u\) 上传的时候,需要将带 \(v\) 那一维的数组也清空了,不然不仅仅是多组数据,单组内也会产生影响。
肆、用到の小 \(\tt trick\)
树上的相邻问题在点分树上是祖先关系,同时,点分树深度只有 \(\log n\),这个不难得出。
任意函数的 \(\min/\max\) 卷积都是 \(\mathcal O(m^2)\) 的,无法进行优化,但是我们可以将合并数组转化为单点加入,这样对于每个点就是 \(\mathcal O(m)\) 的,每个点加一次就是 \(\mathcal O(nm)\) 的,可以将一个 \(m\) 向 \(n\) 偏移,但是这样有可能会少考虑一些情况,一般还需要一些其他的东西对状态进行更完整的记录,比如我们这里需要用一维 \(2^{\log n}\) 保存祖先选择情况,以判断这个点能否被选择。
某些树上的 \(\tt DP\) 可以转化到 \(\tt dfs\) 序上来做,有时候也可以从这方面进行考虑。