【题解】P7800 [COCI2015-2016#6] PAROVI

题目链接cnblogs

题意简述

用满足 \(\gcd(l,r) = 1\) 的线段 \([l,r]\) 完全覆盖区间 \([1,n]\),求方案数。

题目分析

有一个套路:遇到线段覆盖相关问题,线段根据左端点排序后覆盖,覆盖的联通块个数不减。

假设我们已经处理出来了这些合法线段,明显的:\(dp_{i,j}\) 表示前 \(i\) 条线段覆盖了 \([1,j]\) 的方案数。转移就是 \(dp_{i,j} \gets dp_{i-1,j}\) 表示不选这条线段,和 \(dp_{i,j} \gets dp_{i-1,\max(j,r)},j \ge l\) 表示选了这条线段。由于按照左端点排序带来的性质,当 \(j< l\) 时连通块个数增多,那块空出来的不再会被覆盖到,与我们要求的无关,直接不理它。所以有以下代码:

const int N = 25,mod = 1e9;
int f[N*N][N],n,ans;
struct P{ int l,r; };
vector<P> e;

inline int add(int x,int y){ return x + y >= mod ? x + y - mod : x + y; }
inline void toadd(int &x,int y){ x = add(x,y); }

signed main(){
	scanf("%d",&n);
	for(int i = 1;i<=n;++i)
		for(int j = i;j<=n;++j)if(__gcd(i,j) == 1 && i != j)
			e.push_back({i,j});
	int tot = e.size();
	f[0][1] = 1;
	for(int i = 1;i<=tot;++i){
		for(int j = e[i-1].l;j<=n;++j){
			toadd(f[i][j],f[i-1][j]);
			toadd(f[i][max(j,e[i-1].r)],f[i-1][j]);
		}
	}
	printf("%d",f[tot][n]);
	return 0;
}

这已经可以通过了,但数据范围更大一些呢?观察加入每条线段对 \(f\) 的贡献:将 \(r\) 这个位置上加上 \(\sum_{i=l}^rf_i\),将 \([r+1,n]\) 翻倍。我一开始直接就无脑线段树维护,太蠢了。时间复杂度是 \(O(cnt \times \log n)\) 的。

我们的更新顺序是固定左端点向右扫右端点,而我们乘的又是一个后缀,我们加的是一个前缀。所以我们可以对于相同的 \(l\) 一起处理,在右移 \(r\) 的过程中,实时维护当前要乘多少和一个 \(f\) 的前缀和 \(sum\),舍弃了加入线段,而是遍历点。代码如下:

f[1] = 1;
for(int l = 1;l<=n;++l){
	int now = 1;
	sum[l-1] = f[l-1];
	for(int r = l;r<=n;++r){
		if(gcd[l][r] == 1 && l != r){	
			f[r] = mul(f[r], now);
			sum[r] = add(sum[r-1], f[r]);
			toadd(f[r], sub(sum[r], sum[l-1]));
			now = mul(now, 2);
		}else f[r] = mul(f[r], now);
		sum[r] = add(sum[r-1], f[r]);
	}
}
printf("%d",f[n]);

这里的 \(f_i\) 就表示当前覆盖 \([1,i]\) 的方案数。细节:由于 \(sum\) 的意义在发生变化,所以要在必要时更新 \(sum\)。初值由于是 \(f_{1} = 1\),中间转移时判掉 l != r 就是避免这点。最终转移的时间复杂度 \(O(n^2)\)。当然我们可以优化处理 \(gcd\) 的过程,也算是个 trick 吧,从 大佬XuYueming的博客 学的。本质就是辗转相减的逆过程,像遍历这棵二叉树一样用队列实现,代码帖这:

void init(){
	q[++tail] = {1, 1}; ++head;
	q[++tail] = {1, 2};
	while(head <= tail){
		pii cur = q[head++];
		if(cur.second > n)continue;
		q[++tail] = {cur.first, cur.first + cur.second};
		q[++tail] = {cur.second, cur.first + cur.second};
	}
	for(int i = 1;i<=tail;++i)gcd[q[i].first][q[i].second] = 1;
}

总结

  1. 区间覆盖相关的,与连通性相关的,按左端点排序;
  2. 观察式子,同时结合更新过程以优化;
  3. 处理 \(\gcd\) 的技巧。
posted @ 2024-11-22 21:13  Luzexxi  阅读(20)  评论(0)    收藏  举报