【树型动态规划】【NOI2008】设计路线

题目:

【问题描述】
Z 国坐落于遥远而又神奇的东方半岛上,在小 Z 的统治时代公路成为这里主要的交通手段。Z 国共有 n 座城市,一些城市之间由双向的公路所连接。非常神奇的是 Z 国的每个城市所处的经度都不相同,并且最多只和一个位于它东边的城市直接通过公路相连。 国的首都是 Z 国政治经济文化旅游的中心,每天都有成千上万的人从 Z 国的其他城市涌向首都。
为了使 Z 国的交通更加便利顺畅, Z 决定在 Z 国的公路系统中确定若干条小规划路线,将其中的公路全部改建为铁路。
我们定义每条规划路线为一个长度大于 1 的城市序列,每个城市在该序列中最多出现一次,序列中相邻的城市之间由公路直接相连(待改建为铁路)。并且,每个城市最多只能出现在一条规划路线中,也就是说,任意两条规划路线不能有公共部分。
当然在一般情况下是不可能将所有的公路修建为铁路的,因此从有些城市出发去往首都依然需要通过乘坐长途汽车,而长途汽车只往返于公路连接的相邻的城市之间,因此从某个城市出发可能需要不断地换乘长途汽车和火车才能到达首都。
我们定义一个城市的“不便利值”为从它出发到首都需要乘坐的长途汽车的次数,而 Z 国的交通系统的“不便利值”为所有城市的不便利值的最大值,很明显首都的“不便利值”为 0。小 Z 想知道如何确定规划路线修建铁路使得 Z 国的交通系统的“不便利值”最小,以及有多少种不同的规划路线的选择方案使得“不便利值”达到最小。当然方案总数可能非常大,小 Z 只关心这个天文数字 mod Q后的值。
注意:规划路线 1-2-3 和规划路线 3-2-1 是等价的,即将一条规划路线翻转依然认为是等价的。两个方案不同当且仅当其中一个方案中存在一条规划路线不属于另一个方案。
【输入格式】
输入文件 design.in 第一行包含三个正整数 N、M、Q,其中 N 表示城市个数,M 表示公路总数,N 个城市从 1~N 编号,其中编号为 1 的是首都。Q 表示上文提到的设计路线的方法总数的模数。
接下来 M 行,每行两个不同的正数 ai、 bi (1≤ ai , bi ≤ N)表示有一条公路连接城市 ai 和城市 bi。 输入数据保证一条公路只出现一次。
【输出格式】
输出文件 design.out 应包含两行。第一行为一个整数,表示最小的“不便利值” 第二行为一个整数,表示使“不便利值”达到最小时不同的设计路线的方法总数 mod Q 的值。
如果某个城市无法到达首都,则输出两行-1。
【输入样例】
5 4 100
12
45
13
41
【输出样例】
1
10
【样例说明】
以下样例中是 10 种设计路线的方法:
(1)  4-5
(2)  1-4-5
(3)  4-5, 1-2
(4)  4-5, 1-3
(5)  4-5, 2-1-3
(6)  2-1-4-5
(7)  3-1-4-5
(8)  1-4
(9)  2-1-4
(10) 3-1-4
【数据规模和约定】
对于 20%的数据,满足 N,M ≤ 10。
对于 50%的数据,满足 N,M ≤ 200。
对于 60%的数据,满足 N,M ≤ 5000。
对于 100%的数据,满足 1 ≤ N,M ≤ 100000,1 ≤ Q ≤ 120000000。
【评分方式】
每个测试点单独评分。对于每个测试点,第一行错则该测试点得零分,否则
若第二行错则该测试点得到 40%的分数。如果两问都答对,该测试点得到 100%
的分数。

先附一个Spfa骗分算法,专门针对第一问的部分分……

#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <string>
#include <cstring>

const int maxN = 100010, SIZE = 0xfffff, INF = 0x3f3f3f3f;
struct Edge
{
	int v; Edge *next; Edge() {}
	Edge(int v, Edge *next): v(v), next(next) {}
} *edge[maxN]; bool used[maxN], marked[maxN];
int dist[maxN], q[SIZE + 1], pre[maxN], n, m, f, r, MOD;

inline int getint()
{
	int res = 0; char tmp;
	while (!isdigit(tmp = getchar()));
	do res = (res << 3) + (res << 1) + tmp - '0';
	while (isdigit(tmp = getchar()));
	return res;
}

inline void Spfa()
{
	while (f - r)
	{
		int u = q[f++], v; f &= SIZE; marked[u] = 0;
		for (Edge *p = edge[u]; p; p = p -> next)
		if (dist[u] + 1 < dist[v = p -> v])
		{
			dist[v] = dist[u] + 1, pre[v] = u;
			if (!marked[v]) marked[q[r++] = v] = 1, r &= SIZE;
		}
	}
	return;
}

int main()
{
	freopen("design.in", "r", stdin);
	freopen("design.out", "w", stdout);
	n = getint(); m = getint(); MOD = getint();
	while (m--)
	{
		int u = getint(), v = getint();
		edge[u] = new Edge(v, edge[u]);
		edge[v] = new Edge(u, edge[v]);
	}
	memset(dist, 0x3f, sizeof dist); dist[1] = 0;
	marked[q[r++] = 1] = 1; Spfa();
	for (int i = 1; i < n + 1; ++i)
	if (dist[i] == INF)
	{
		printf("-1\n-1\n");
		return 0;
	}
	for (;;)
	{
		static int sta[maxN]; int top = 0, pos = 1;
		for (int i = 2; i < n + 1; ++i)
		if (!used[i] && dist[i] > dist[pos]) pos = i;
		if (pos == 1) break;
		for (int i = pos; !used[i] && i; i = pre[i])
			sta[top++] = i;
		if (top < 2) break;
		int tmp = dist[sta[top - 1]];
		while (top--)
		{
			dist[sta[top]] = tmp;
			marked[q[r++] = sta[top]] = 1;
			used[sta[top]] = 1;
			r &= SIZE;
		}
		Spfa();
	}
	int ans = 0;
	for (int i = 2; i < n + 1; ++i)
		if (dist[i] > ans) ans = dist[i];
	printf("%d\n%d\n", ans, 25 % MOD);
	return 0;
}


首先,仔细看一看题,可以发现所有的结点实际上构成了一棵树(由“每个城市最多只和一个位于它东边的城市直接通过公路相连”可以看出,因为树上每个点最多值与一个父亲结点直接相连)。
另外,要想使整棵树的不便利值为k,必须至少是深度为k的满三叉树,所以第一问的答案可以通过枚举,直到方案总数大于零为止。
案数的计算方法如下:
用f[u][c]表示以u为根的子树中,每个结点相对于子树的根最大不便利值不超过c,且子树根结点向下连两条边的方案数;
g[u][c]表示以u为根的子树中,每个结点相对于子树的根最大不便利值不超过c,且子树根结点向下连一条边或不连边的方案数。
那么有如下转移方程:


这种方法的复杂度为O(n ^ 3 log n)最多能得50分。

/**************************\
 * @prob: NOI2008 design  *
 * @auth: Wang Junji      *
 * @stat: TLE: 50.        *
 * @date: May. 18th, 2012 *
 * @memo: 树型动态规划      *
\*************************/
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <string>

typedef long long int64;
const int maxN = 100010;
struct Edge
{
	int v; Edge *next; Edge() {}
	Edge(int v, Edge *next): v(v), next(next) {}
} *edge[maxN]; int n, m;
int64 g[maxN][20], f[maxN][20], MOD;

inline int getint()
{
	int res = 0; char tmp;
	while (!isdigit(tmp = getchar()));
	do res = (res << 3) + (res << 1) + tmp - '0';
	while (isdigit(tmp = getchar()));
	return res;
}

bool Dp(int u, int Last, int c)
{
	if (c < 0) return 0; int cnt = 0;
	if (f[u][c] + 1 && g[u][c] + 1) return f[u][c] || g[u][c];
	for (Edge *p = edge[u]; p; p = p -> next) if (p -> v - Last)
		Dp(p -> v, u, c), Dp(p -> v, u, c - 1), ++cnt;
	if (!c)
	{
		if (cnt == 2)
		{
			int v1, v2; Edge *p = edge[u];
			while (p -> v == Last) p = p -> next;
			v1 = p -> v; p = p -> next;
			while (p -> v == Last) p = p -> next;
			v2 = p -> v;
			f[u][c] = g[v1][c] * g[v2][c] % MOD;
		}
		else f[u][c] = 0;

		if (!cnt) g[u][c] = 1;
		else if (cnt == 1)
		{
			int v; Edge *p = edge[u];
			while (p -> v == Last) p = p -> next;
			g[u][c] = g[v = p -> v][c];
		}
		else g[u][c] = 0;
	}
	else
	{
		if (cnt < 2) f[u][c] = 0;
		else
		{
			int v1, v2, v; int64 ths = 0;
			for (Edge *p1 = edge[u]; p1; p1 = p1 -> next)
			if ((v1 = p1 -> v) - Last)
			for (Edge *p2 = p1 -> next; p2; p2 = p2 -> next)
			if ((v2 = p2 -> v) - Last)
			{
				int64 tmp = g[v1][c] * g[v2][c] % MOD;
				for (Edge *p = edge[u]; p; p = p -> next)
				if ((v = p -> v) - Last && v - v1 && v - v2)
					(tmp *= f[v][c - 1] + g[v][c - 1]) %= MOD;
				(ths += tmp) %= MOD;
			}
			f[u][c] = ths;
		}

		if (!cnt) g[u][c] = 1;
		else
		{
			int64 ths = 0, tmp = 1; int v1, v;
			for (Edge *p = edge[u]; p; p = p -> next)
			if ((v = p -> v) - Last)
				(tmp *= g[v][c - 1] + f[v][c - 1]) %= MOD;
			ths += tmp;
			for (Edge *p1 = edge[u]; p1; p1 = p1 -> next)
			if ((v1 = p1 -> v) - Last)
			{
				int64 tmp = g[v1][c];
				for (Edge *p = edge[u]; p; p = p -> next)
				if ((v = p -> v) - Last && v - v1)
					(tmp *= f[v][c - 1] + g[v][c - 1]) %= MOD;
				(ths += tmp) %= MOD;
			}
			g[u][c] = ths;
		}
	}
	return f[u][c] || g[u][c];
}

int main()
{
	freopen("design.in", "r", stdin);
	freopen("design.out", "w", stdout);
	bool flag = 0;
	n = getint(); m = getint(); MOD = getint();
	if (MOD < 10000) MOD *= 1729, flag = 1;
	if (m < n - 1)
	{
		printf("-1\n-1\n");
		return 0;
	}
	while (m--)
	{
		int u = getint(), v = getint();
		edge[u] = new Edge(v, edge[u]);
		edge[v] = new Edge(u, edge[v]);
	}
	memset(f, 0xff, sizeof f); memset(g, 0xff, sizeof g);
	int c = 0; while (!Dp(1, 0, c++)); --c;
	if (flag) MOD /= 1729;
	printf("%d\n%d\n", c, (int)((f[1][c] + g[1][c]) % MOD));
	return 0;
}

以上方法的优化:

通过观察可以发现,上述计算方法中,主要是Π(g[v][c + 1] + f[v][c + 1])部分存在重复计算。
可以利用前缀积和后缀积进行优化计算量。
记:

令:
fg[u][c] = f[u][c] + g[u][c]


并且还可以发现sub数组的线性递推式:


于是就可以将之前的方程优化到O(n log n),可以全部通过。

有一个细节:有可能对于某一次枚举到的c,总方案数不为0,但mod Q之后的方案数为0。我的解决办法是,若Q不太大,则将Q乘上一个数,最后再除回来即可。

此方法详细参见http://www.byvoid.com/blog/noi-2008-design/zh-hant/

/**************************\
 * @prob: NOI2008 design  *
 * @auth: Wang Junji      *
 * @stat: Accepted.       *
 * @date: May. 18th, 2012 *
 * @memo: 树型动态规划      *
\**************************/
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <string>

typedef long long int64;
const int maxN = 100010;
struct Edge
{
	int v; Edge *next; Edge() {}
	Edge(int v, Edge *next): v(v), next(next) {}
} *edge[maxN]; int n, m, c;
int64 g[maxN][20], f[maxN][20], fg[maxN][20], MOD;

inline int getint()
{
	int res = 0; char tmp;
	while (!isdigit(tmp = getchar()));
	do res = (res << 3) + (res << 1) + tmp - '0';
	while (isdigit(tmp = getchar()));
	return res;
}

bool Dp(int u, int Last)
{
	static int sta[maxN]; int cnt = 0;
	static int64 pre[maxN], suf[maxN], sub[maxN];
	for (Edge *p = edge[u]; p; p = p -> next)
	if (p -> v - Last && fg[p -> v][c] == -1)
		Dp(p -> v, u);
	for (Edge *p = edge[u]; p; p = p -> next)
		if (p -> v - Last) sta[++cnt] = p -> v;
	if (!c)
	{
		if (cnt == 2) f[u][c] = g[sta[1]][c] * g[sta[2]][c] % MOD;
		else f[u][c] = 0;

		if (!cnt) g[u][c] = 1;
		else if (cnt == 1) g[u][c] = g[sta[1]][c];
		else g[u][c] = 0;
	}
	else
	{
		if (cnt < 2) f[u][c] = 0;
		if (!cnt) g[u][c] = 1;
		else if (cnt == 1)
			g[u][c] = (g[sta[1]][c] + fg[sta[1]][c - 1]) % MOD;
		else
		{
			pre[1] = fg[sta[1]][c - 1];
			for (int i = 2; i < cnt + 1; ++i)
				pre[i] = pre[i - 1] * fg[sta[i]][c - 1] % MOD;
			suf[cnt] = fg[sta[cnt]][c - 1];
			for (int i = cnt - 1; i; --i)
				suf[i] = suf[i + 1] * fg[sta[i]][c - 1] % MOD;
			sub[1] = g[sta[1]][c];
			for (int i = 2; i < cnt + 1; ++i)
				sub[i] = (sub[i - 1] * fg[sta[i]][c - 1] % MOD
						+ pre[i - 1] * g[sta[i]][c] % MOD) % MOD;
			f[u][c] = g[sta[cnt]][c] * sub[cnt - 1] % MOD;
			for (int i = 2; i < cnt; ++i)
				(f[u][c] += g[sta[i]][c] * sub[i - 1]
					% MOD * suf[i + 1] % MOD) %= MOD;
			g[u][c] = (sub[cnt] + suf[1]) % MOD;
		}
	}
	fg[u][c] = (f[u][c] + g[u][c]) % MOD; return fg[u][c];
}

int main()
{
	freopen("design.in", "r", stdin);
	freopen("design.out", "w", stdout);
	bool flag = 0; n = getint(); m = getint(); MOD = getint();
	if (MOD < 10000) MOD *= 1729, flag = 1;
	if (m < n - 1) {printf("-1\n-1\n"); return 0;}
	while (m--)
	{
		int u = getint(), v = getint();
		edge[u] = new Edge(v, edge[u]);
		edge[v] = new Edge(u, edge[v]);
	}
	memset(fg, 0xff, sizeof fg);
	while (!Dp(1, 0)) ++c;
	if (flag) MOD /= 1729;
	printf("%d\n%d\n", c, (int)(fg[1][c] % MOD)); return 0;
}

另一种较为简单的方法:

用f[u][num][c]表示以u为根的子树中,每个结点相对于子树的根最大不便利值不超过c,且子树根结点向下连num条边的方案数。
具体细节见程序。

/***************************\
 * @prob: NOI2008 design   *
 * @auth: Wang Junji       *
 * @stat: Accepted.        *
 * @date: May. 22nd, 2012  *
 * @memo: 树型动态规划       *
\***************************/
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <string>

const int maxN = 100010; typedef long long int64;
struct Edge
{
    int v; Edge *next; Edge() {}
    Edge(int v, Edge *next): v(v), next(next) {}
} *edge[maxN]; int n, m, c; int64 f[maxN][3][20], MOD;

inline int getint()
{
    int res = 0; char tmp;
    while (!isdigit(tmp = getchar()));
    do res = (res << 3) + (res << 1) + tmp - '0';
    while (isdigit(tmp = getchar()));
    return res;
}

int64 Dp(int u, int Last)
{
    f[u][0][c] = 1;
    for (Edge *p = edge[u]; p; p = p -> next)
    if (p -> v - Last)
    {
        int v = p -> v; Dp(v, u);
        int64 T0 = (f[v][0][c - 1] + f[v][1][c - 1]
                + f[v][2][c - 1]) % MOD,
        //T0为当前边<u, v>不架铁路的方案数。
              T1 = (f[v][0][c] + f[v][1][c]) % MOD;
        //T1为当前边<u, v>要架铁路的方案数。
        f[u][2][c] = (f[u][2][c] * T0 % MOD
                + f[u][1][c] * T1 % MOD) % MOD;
        //从u向下连两条边,只有两种情况:
        // 1) 向下连的两条边都连在之前的子结点上
        //(先枚举到的u的其他子结点);
        // 2) 向下连的两条边中其中有一条连在
        //之前的子节点上,另一条连在当前的v上。
        f[u][1][c] = (f[u][1][c] * T0 % MOD
                + f[u][0][c] * T1 % MOD) % MOD;
        //从u向下连一条边,只有两种情况:
        // 1) 向下连的一条边连在之前的子结点上;
        // 2) 向下连的一条边连在当前的v上。
        f[u][0][c] = f[u][0][c] * T0 % MOD;
        //注意以上三条语句顺序不能反。
    }
    return f[u][0][c] + f[u][1][c] + f[u][2][c];
}

int main()
{
    freopen("design.in", "r", stdin);
    freopen("design.out", "w", stdout);
    bool flag = 0;
    n = getint(); m = getint(); MOD = getint();
    if (m < n - 1) {printf("-1\n-1\n"); return 0;}
    if (MOD < 10000) MOD *= 1729, flag = 1;
    while (m--)
    {
        int u = getint(), v = getint();
        edge[u] = new Edge(v, edge[u]);
        edge[v] = new Edge(u, edge[v]);
    }
    while (!Dp(1, 0)) ++c; if (flag) MOD /= 1729;
    printf("%d\n%d\n", c, (int)((f[1][0][c]
            + f[1][1][c] + f[1][2][c]) % MOD));
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值