[HDU6566]The Hanged Man

话说这一套题的题目好像都是以塔罗牌为名字的啊......而且这个题目和倒吊人有什么关系?

壹、题目描述

传送门 to HDU

贰、题解

首先想到一个比较朴素的树 \(\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\) 序上来做,有时候也可以从这方面进行考虑。

posted @ 2021-02-23 22:21  Arextre  阅读(73)  评论(0编辑  收藏  举报