[NOI2013]树的计数 计数Dp

[NOI2013]树的计数

题目传送门

分析

强烈吐槽一下这道题不采用取模的方式,害我被卡精度卡了半天。

因为毕竟是是自己yy的一种垃圾做法

手玩几组数据~~(或者左转看看题解)~~发现按层划分BFS序很科学。

然后就可以发现一个结论:一种合法的BFS序的划分对应一棵树

证明:考虑递归构造对应关系。对于某个BFS序,如果我们已经构造出了 i i i层以内的树,考虑第 i + 1 i+1 i+1层,因为已经划分好了,所以这一层有那些节点我们是知道的。如果我们将 i i i层以内的节点在DFS序中全部删掉,剩下的一定是DFS序中的若干个区间。DFS序可以看成子树根+子树的形式,那么每个区间就对应一个子树,并且子树根就是区间左端点前一个节点。这样的话 B F S BFS BFS序中第 i + 1 i+1 i+1层的节点就可以去对应区间找他们的父亲。

那么现在问题转化成合法地划分 B F S BFS BFS序。考虑 D p Dp Dp

f i , d f_{i,d} fi,d表示划分到第 i i i个数,一共划分了 d d d层的方案。

f i , d = ∑ j = 1 i − 1 f j , d − 1 [ c a n ( j , i ) ] f_{i,d}=\sum_{j=1}^{i-1}f_{j,d-1}[can(j,i)] fi,d=j=1i1fj,d1[can(j,i)]

c a n ( j , i ) can(j,i) can(j,i)表示 [ j + 1 , i ] [j+1,i] [j+1,i]这个划分是合法的。

考虑如何求出 c a n ( j , i ) can(j,i) can(j,i)

在构造的时候实际上已经初步得出了一个层划分合法性的判定方法。

首先将BfS序中 j j j以前的点删掉。

根据题目的要求,第一个条件就是BfS序中 [ j + 1 , i ] [j+1,i] [j+1,i]中的点在DFS序中的位置递增。

而构造的时候发现,删去的区间左端点一定是子树根,所以必须出现在划分中。

如果暴力枚举 i , j i,j i,j,然后 O ( n ) O(n) O(n)判断,这样的求 c a n can can复杂度是 O ( n 3 ) O(n^3) O(n3)的,而 D p Dp Dp也是 O ( n 3 ) O(n^3) O(n3)的,这样就得到了一个 O ( n 3 ) O(n^3) O(n3)的算法,接下来考虑优化。

首先,往 O ( n 2 ) O(n^2) O(n2)进发,先优化预处理 c a n ( j , i ) can(j,i) can(j,i)

先枚举一个 j j j,然后枚举 i i i,对于第2个条件,我们对于 D F S DFS DFS序中每个区间左端点在 B F S BFS BFS序中的最大位置 l l l,那么合法的 i i i一定在 l l l之后。

然后从 l l l开始扫描一段区间,这段区间满足条件1,也就是位置递增即可。假设扫到了 r r r,那么合法 i i i [ l , r ] [l,r] [l,r]之间。

对于 f f f,一种比较有技巧性的做法是用生成函数优化。

F i ( x ) = ∑ f i , d x d F_i(x)=\sum f_{i,d}x^d Fi(x)=fi,dxd

这样的话方程可以写成 F i = ∑ x F j [ c a n ( j , i ) ] F_i=\sum xF_j[can(j,i)] Fi=xFj[can(j,i)]

事实上,我们要求的东西是 F n ′ ( 1 ) F n ( 1 ) \frac{F'_n(1)}{F_n(1)} Fn(1)Fn(1)

求个导看看:

F i ′ = x ∑ F j ′ [ c a n ( j , i ) ] + ∑ F j [ c a n ( j , i ) ] F_i'=x\sum F_j'[can(j,i)]+\sum F_j[can(j,i)] Fi=xFj[can(j,i)]+Fj[can(j,i)]

x = 1 x=1 x=1带入:

F i ( 1 ) = ∑ F j ( 1 ) [ c a n ( j , i ) ] F_i(1)=\sum F_j(1)[can(j,i)] Fi(1)=Fj(1)[can(j,i)]

F i ′ ( 1 ) = ∑ F j ′ ( 1 ) [ c a n ( j , i ) ] + ∑ F j ( 1 ) [ c a n ( j , i ) ] F'_i(1)=\sum F'_j(1)[can(j,i)]+\sum F_j(1)[can(j,i)] Fi(1)=Fj(1)[can(j,i)]+Fj(1)[can(j,i)]

f i = F i ( 1 ) , g i = F i ′ ( 1 ) f_i=F_i(1),g_i=F'_i(1) fi=Fi(1),gi=Fi(1)转移就行了。

其实这个东西可以直接用期望去推,但是人菜就只好用笨方法推方程咯!

所以成功地把 D p Dp Dp也优化到了 O ( n 2 ) O(n^2) O(n2)

还可以优化吗?

之前发现的一个性质是,对于一个 j j j,它仅仅会转移到一段特定区间 [ l , r ] [l,r] [l,r]内的 i i i。而且贡献是线性的。

那么显然 D p Dp Dp部分差分打标记前缀和就 O ( n ) O(n) O(n)了。

c a n ( j , i ) can(j,i) can(j,i)怎么办?

对于某个 j j j,显然 r r r是可以用指针扫过去的。除非当前的 j j j越过了 r r r,不然 r r r不会动。

l l l可以在删除数的时候记录一下删除位置的下一个位置对应BFS序的位置,取个 m a x max max即可。

这样的话两把指针扫一扫也是线性的了。这道题就这么被胡乱优化一波搞定了!

结果被卡精度了?!!!

开__float128也不行?!!!!

我搞了一波骚操作,把某个数科学计数法表示,然后重载加减法,忽略数量级超过一百的运算,然后就过了。。。。

代码

#include<bits/stdc++.h>
const int N = 2e5 + 10;
int ri() {
	char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
	for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
struct Data {
	double x; int e;
}g[N], f[N];
Data operator + (Data a, Data b) {
	if(a.e < b.e)
		std::swap(a, b);
	int d = a.e - b.e; double l = b.x;
	if(d > 100) l = 0;
	else {
		for(;d--;)
			l /= 10;
	}
	a.x += l;
	if(fabs(a.x) > 10) a.x /= 10, ++a.e;
	return a;
}
void operator += (Data &a, Data b) {a = a + b;}
void operator -= (Data &a, Data b) {
	b.x = -b.x;
	a = a + b;
}
int a[N], b[N], psa[N], psb[N], n, must;
int main() {
	n = ri();
	for(int i = 1;i <= n; ++i)
		a[i] = ri(), psa[a[i]] = i;
	for(int i = 1;i <= n; ++i)
		b[i] = ri(), psb[b[i]] = i;
	int l = 0, r = 0;
	f[1].x = g[1].x = 1;
	f[2].x = g[2].x = -1;
	for(int j = 1, i;j <= n; ++j) {
		f[j] += f[j - 1]; g[j] += g[j - 1];
		if(j == n) break;
		l = std::max(l, psb[a[psa[b[j]] + 1]]);
		for(r = std::max(r, j + 2);r <= n; ++r)
			if(psa[b[r]] < psa[b[r - 1]])
				break;
		--r;
		if(l <= r) {
			f[l] += f[j]; f[r + 1] -= f[j];
			g[l] += f[j] + g[j]; g[r + 1] -= f[j] + g[j];
		}
	}
	int d = g[n].e - f[n].e; double x = g[n].x / f[n].x;
	for(;d--;)
		x *= 10;
	printf("%.3lf\n", x);
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值