hihoCoder #1471 拥堵的城市

这道题目是hihoCoder Challenge 27的C题,我考虑了5天:(。
计数问题。由于树的结构的特殊性(树具有递归结构),不难想到思路是树形DP。由于这是【计数问题】而非【优化问题】,我们思考时应该着重考虑两个基本的计数原理:加法原理与乘法原理,而非所谓【最优子结构】。

DP 状态

关于DP状态设计,我的思考过程如下:
指定一节点作为根,转化成有根树。我们考虑子问题:

子树 $v$ 中的路线方案数。

然后考虑

将子树 $v$ 中的所有可能的方案压缩到一个怎样的子空间中能实现从(子树) $v$ 到其父节点 (子树)$u$ 的状态转移?

DP状态要有助于计算通过 $u$ 的路线的方案数。而这样的路线由任意两不同的自由节点(非路线端点) $v_1$、$v_2 \text{ s.t. } \text{LCA}(v_1,v_2) = u$ 所确定。

状态1
$dp[i][j]$:子树 $i$ 中有 $j$ 个自由点的方案数。

然而这 $j$ 个自由点到点 $i$ 的路径在【任意一条边最多被 $k$ 条路线经过】的限制下可能互斥。

状态2

在 $dp[i][j]$: 从子树 $i$ 中选择 $j$ 条【独立路径】伸出来的方案数。

这个状态是可以转移的。这里需要指出的一点是,我们不采用类似于 “子树 $i$ 中有 $j$ 条独立路径伸出来” 这样的表述来定义状态,而采用 “选择” 这个词。

转移方程

同大多数树形 DP 一样,状态转移是一个合并子树的过程。在计算子树 $u$ 中的方案数的过程中,对于 $u$ 的儿子 $v$,将子树 $v$ 的状态合并到子树 $u$ 中的方法:

枚举从子树 $v$ 中选出的【用于和前面的独立路径配对的独立路径】的数目 $x$ 和【不配对的独立路径】的数目 $y$

Implementation

#include <bits/stdc++.h>
using namespace std;

using ll = long long;

const int mod = 1e9+7;

const int N = 305;

vector<int> g[N];
int size[N];

int dp[N][N];


void add_mod(int &x, int y)
{
	x += y;
	if(x >= mod)
		x -= mod;
}

int f[N]{1};
int c[N][N];

void prep(int n)
{
	for(int i=1; i<=n; ++i)
		f[i]=ll(f[i-1])*i%mod;

	for(int i=0; i<=n; i++)
		c[i][0] = 1;
	for(int i=1; i<=n; i++)
		for(int j=1; j<=i; j++)
			add_mod(c[i][j], c[i-1][j-1]),  add_mod(c[i][j], c[i-1][j]);
}

int cur[N];

int n, k;

void dfs(int u, int fa)
{
	size[u] = 1;
	dp[u][0] = 1;
	dp[u][1] = 1;

	for(auto &v: g[u])
	{
		if(v != fa)
		{
			dfs(v, u);
			for(int x=0; x <= min(size[v], k); ++x)	// pair
			{
				for(int y=0; x+y <= min(size[v], k); ++y)	// single
				{
					int tmp = ll(dp[v][x+y]) * c[x+y][y] % mod;
					
					if(tmp == 0)
						continue;

					for(int i=x; i<=size[u]; ++i)
					{
						add_mod(cur[i-x+y], ll(tmp) * dp[u][i] % mod * c[i][x] % mod * f[x] % mod);	// error-prone
					}
				}
			}

			size[u] += size[v];

			for(int i=0; i<=size[u]; ++i)
			{
				dp[u][i] = cur[i];
				cur[i] = 0;
			}		
		}
	}
}

int main()
{
	scanf("%d%d", &n, &k);
	prep(n);
	for(int i=1; i<n; i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		g[u].push_back(v);
		g[v].push_back(u);	
	}

	dfs(1, 1);
	
	printf("%d\n", dp[1][0]);
	return 0;
}

要格外注意递归函数中的全局变量。

总结

前两天在知乎上看到一个解释【DP状态的无后效性】的回答,思考这个问题的过程中感到很受用。

一般我们说的最优解问题,最朴素的做法是搜索所有的方案,大部分时候这个复杂度是不能接受的。对某些问题来说,我们可以将问题分解成子问题,子问题具有最优子结构的特点:对子问题的优化可以带来对整个问题的优化。但仅仅是这样并没有很大的帮助,因为我们对子问题的优化可能会导致整个问题的其他部分的解法变得不成立,或者变得不够优化,这样我们仍然不能有效计算整个问题的最优解。但是,某些情况下,子问题对于整个问题的影响,可以归纳到一个比“所有子问题解法”的空间小得多的“子问题状态”空间中,只要子问题解决后的状态处于某个状态,它对于整个问题的影响就是一致的,这样如果已知子问题处于某个状态,我们就可以求出这个状态条件下子问题的最优解,当全局最优且这个子问题取这个状态时,子问题本身的解法一定是我们刚刚求出的最优解。这样我们只需要对每个子问题的每个状态计算最优解,然后通过这些最优解的递推关系就可以求出整个问题的最优解。这种子问题对主问题的影响可以总结为少数“状态”的特性就叫做无后效性,意味着只要状态一致,具体经过怎样的步骤到达这一状态是没有影响的。
SOURCE

posted @ 2017-02-25 10:11  Pat  阅读(336)  评论(0编辑  收藏  举报