树数树

题目描述

牛牛有一棵 \(n\) 个点的有根树,根为 \(1\)。 我们称一个长度为 \(m\) 的序列 \(a\) 是好的,当且仅当:

  • \(∀𝑖 ∈(1, 𝑚]\)\(a_i\)\(a_{i-1}\) 有祖孙关系。

  • \(∀1 ≤𝑖 < 𝑗≤𝑚,𝑎_𝑖 ≠𝑎_𝑗\)

你需要帮助牛牛求出最长的序列长度。

\(n\le 10^5\)

解法

题目中关键的条件是 "这是一棵根为 \(1\) 的有根树"。先开始以为是无根树,难度就不太对劲了…

"完全二叉树" 的部分分提示了正解:子树根节点可以且最多也只能将子树的两个序列合并。做法就很明显了。但是我们需要对每个点维护一个堆存放可行的序列长度,而不是直接用 \(dp\) 数组取最大值,因为祖先也可以合并子孙内部的序列,而不是单单合并自己的儿子。这里偷懒写了启发式合并 + 优先队列,是 \(\mathcal O(n\log^2 n)\) 的。左偏树可以更优。

代码

#include <cstdio>
#define print(x,y) write(x),putchar(y)
template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>'9' or s<'0')
		f |= (s=='-');
	while(s>='0' and s<='9')	
		x = (x<<1)+(x<<3)+(s^48),
		s = getchar();
	return f?-x:x;
}
template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar('-');
		write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
}

#include <queue>
#include <vector>
#include <iostream>
using namespace std;

const int maxn = 1e5+5;

int n,rt[maxn];
vector <int> e[maxn];
priority_queue <int> q[maxn];

int merge(int x,int y) {
	if(q[x].size()<q[y].size())
		swap(x,y);
	while(!q[y].empty())
		q[x].push(q[y].top()),
		q[y].pop();
	return x;
}

void dfs(int u,int fa) {
	rt[u]=u;
	for(int i=0;i<e[u].size();++i) {
		int v=e[u][i];
		if(v==fa) continue;
		dfs(v,u);
		rt[u]=merge(rt[u],rt[v]);
	}
	if(q[rt[u]].empty())
		q[rt[u]].push(1);
	else {
		int t=q[rt[u]].top(); q[rt[u]].pop();
		if(!q[rt[u]].empty())
			t+=q[rt[u]].top(),q[rt[u]].pop();
		q[rt[u]].push(t+1);
	}
}

int main() {
	for(int T=read(9);T;--T) {
		n=read(9);
		for(int i=1;i<=n;++i) {
			e[i].clear();
			while(!q[i].empty())
				q[i].pop();
		}
		for(int i=1;i<n;++i) {
			int u,v;
			u=read(9),v=read(9);
			e[u].push_back(v);
			e[v].push_back(u);
		}
		dfs(1,0);
		print(q[rt[1]].top(),'\n');
	}
	return 0;
}

方格计数

题目描述

在左下角是 \((0,0)\),右上角是 \((W,H)\) 的网格上,有 \((W+1)\times (H+1)\) 个格点。

现在要在格点上找 \(N\) 个不同的点,使得这些点在一条直线上。并且在这条直线上,相邻点之间的距离不小于 \(D\)。求方案数模 \(10^9+7\)

\(1 \leq N \leq 50, W, H, D \leq 500, T \leq 20\)

解法

之前一直在想枚举斜率,然后就死了。其实好像也可以做,但是我不愿再想了

只用枚举 \((i,j)\) 为长和宽,计算这一组的答案,然后再乘上 \((W-i+1)(H-j+1)\) 即可,注意除了 \(i,j\) 有一个等于 \(0\) 的情况,其它的都有斜上与斜下两个方向。

考虑在长度为 \(n\) 的序列中放置 \(m\) 个数,每两个数之间必须至少有 \(k\) 个空格的方案数。考虑相邻有 \(m-1\) 对,空格必须有 \((m-1)\cdot k\) 个。去掉这些位置再自由选择,就是 \(\binom{n-(m-1)\cdot k}{m}\)

回到这道题,对于 \((i,j)\),钦定边界的两个点被选,这样就可以保证不会重。计算出它们 之间 的点数 \(dots=\gcd(i,j)-1\),每两个点至少隔 \(\text{gap}-1\) 个点。由于钦定边界会导致一些点不能被选择,组合数就是

\[\binom{dots-(\text{gap}-1)\cdot 2-(n-3)\cdot (\text{gap}-1)}{n-2} \]

代码

#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>'9' or s<'0')
		f |= (s=='-');
	while(s>='0' and s<='9')
		x = (x<<1)+(x<<3)+(s^48),
		s = getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar('-');
		write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
}

#include <cmath>
#include <iostream>
using namespace std;

const int mod = 1e9+7;

int n,w,h,C[3005][3005];
double d;

int gcd(int x,int y) {
	return y?gcd(y,x%y):x;
}

inline int inc(int x,int y) {
	return x+y>=mod?x+y-mod:x+y;
}

double dis(int x,int y) {
	return sqrt(1.0*x*x+1.0*y*y);
}

int calc(int a,int b) {
	if(a==0 and b==0) return 0;
	int dots=gcd(a,b)-1;
	double D=dis(a/(dots+1),b/(dots+1));
	int gap=d/D;
	if(D*gap<d) ++gap;
	if(dots-(gap-1)*2-(n-3)*(gap-1)<0)
		return 0;
	int ret=C[dots-(gap-1)*2-(n-3)*(gap-1)][n-2];
	if(a and b) ret=2ll*ret%mod;
	return 1ll*ret*(w-a+1)%mod*(h-b+1)%mod;
}

void init() {
	C[0][0]=1;
	for(int i=1;i<=3000;++i) {
		C[i][0]=1;
		for(int j=1;j<=i;++j)
			C[i][j]=inc(C[i-1][j-1],C[i-1][j]);
	}
}

int main() {
	init();
	for(int T=read(9);T;--T) {
		n=read(9),w=read(9);
		h=read(9),d=read(9);
		if(n==1) {
			print((w+1)*(h+1)%mod,'\n');
			continue;
		}
		int ans=0;
		for(int i=0;i<=w;++i)
			for(int j=0;j<=h;++j)
				ans=inc(ans,calc(i,j));
		print(ans,'\n');
	}
	return 0;
}
posted on 2021-10-08 21:13  Oxide  阅读(63)  评论(0编辑  收藏  举报