【Luogu P3214】[HNOI 2011] 卡农

题目链接

题目描述

众所周知卡农是一种复调音乐的写作技法,小余在听卡农音乐时灵感大发,发明了一种新的音乐谱写规则。他将声音分成 n 个音阶,并将音乐分成若干个片段。音乐的每个片段都是由 1 到 n 个音阶构成的和声,即从 n 个音阶中挑选若干个音阶同时演奏出来。为了强调与卡农的不同,他规定任意两个片段所包含的音阶集合都不同。同时为了保持音乐的规律性,他还规定在一段音乐中每个音阶被奏响的次数为偶数。现在的问题是:小余想知道包含 m 个片段的音乐一共有多少种。两段音乐 a 和 b 同种当且仅当将 a 的片段重新排列后可以得到 b。例如:假设 a

为{{1,2},{2,3}},b 为{{3,2},{2,1}},那么 a 与 b 就是同种音乐。由于种数很多,你只需要

输出答案模 100000007(质数)的结果。

Sol

数据范围 10^6
并且是计数题 , 复杂度估计是 O ( n ) O(n) O(n)

说实话 , 要是思路不够清晰的话这题真的很难想到

最后答案要满足三个条件:

  1. 所有集合不重复
  2. 所有集合非空
  3. 所有数字出现次数为偶数
  4. 要求的是无序的排列数

无序的排列在有限制条件下很不好求 , 因为情况太多
但是首先我们发现集合两两不同 , 那么无序这个条件就很好解决了 , 直接在最后有序的答案中乘上阶乘逆元就可以了

关键是上面的三个条件

注意到一个很重要的性质: 每一个片段中一种数字只能出现一次 , 且不能为空集!!!

这意味这在一个合法的方案中再要求加一个片段 , 那么一定是不合法的
从这也可以看出 , 如果已经确定了一些片段 , 要求加入一个片段后合法的方案是唯一的!

那么可以考虑容斥 , 用总数减去不合法的
假设我们要求 m 个片段的方案

那么满足所有数字出现次数为偶数的总方案数就是 A 2 n − 1 m − 1 A_{2^n-1}^{m-1} A2n1m1 , 即从总的可能的集合中选取 m-1 个后排列的方案数 (先把不好处理的条件先限制好)

然后不合法的就是这个时候 , 集合重复出现或者该集合为空的方案

首先处理集合为空的情况 , 如果要选空集才能合法 , 那么必定本来就是合法的 , 所以这部分不合法的方案数 就是 m-1 个片段时的方案数

然后是集合重复的情况 , 这里我们发现如果集合重复出现 , 那么删去这两个集合后仍然合法

于是可以从 m-2 个片段时转移过来
总转移方程:
f [ i ] = A 2 n − 1 i − 1 − f [ i − 1 ] − f [ i − 2 ] ∗ ( i − 1 ) ∗ ( 2 n − i + 1 ) f[i]=A_{2^n-1}^{i-1} -f[i-1]-f[i-2]*(i-1)*(2^n-i+1) f[i]=A2n1i1f[i1]f[i2](i1)(2ni+1)

代码:

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
int n,m;
typedef long long ll;
const ll mod=1e8+7;
const int N=1e6+10;
inline ll fpow(ll x,ll k)
{
	register ll ret=1;
	while(k){
		if(k&1) ret=ret*x%mod;
		x=x*x%mod;
		k>>=1;
	}
	return ret;
}
ll f[N];
int main()
{
	scanf("%d %d",&n,&m);
	if(n==1) return puts("0"),0;
	if(m<=2) return puts("0"),0;
	register ll fac=1;
	for(register int i=1;i<=m;++i) fac=fac*i%mod;
	register ll inv=fpow(fac,mod-2);
	register ll S=(fpow(2,n)+mod-1)%mod;
	register ll A=S;
	for(register ll i=3;i<=m;++i) {
		register ll T=(S-i+2+mod)%mod;
		A=A*T%mod;
		f[i]=(A-f[i-1]-f[i-2]*(i-1)%mod*T%mod+(mod<<1))%mod;
	}
	f[m]=f[m]*inv%mod;
	printf("%lld\n",f[m]);
}

洛谷P1177是【模板】排序题,可使用归并排序来解决。归并排序的核心思想是分治法,即将一个大问题分解为多个小问题,分别解决后再合并结果。 归并排序主要步骤如下: 1. **分解**:将待排序数组从中间分成两个子数组,递归地对这两个子数组进行排序。 2. **合并**:将两个已排序的子数组合并成一个有序数组。 以下是使用归并排序解决洛谷P1177题目的代码实现: ```cpp #include<bits/stdc++.h> #include<iomanip> using namespace std; #define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) const int MAXN = 1e5 + 5; int a[MAXN], b[MAXN]; int n; // 数组长度 // 合并两个已排序的子数组 void mergesort(int l1, int r1, int l2, int r2) { int i = l1, j = l2, k = l1; while (i <= r1 && j <= r2) { if (a[i] <= a[j]) { b[k++] = a[i++]; } else { b[k++] = a[j++]; } } while (i <= r1) b[k++] = a[i++]; while (j <= r2) b[k++] = a[j++]; for (i = l1; i <= r2; i++) { a[i] = b[i]; } } // 递归进行归并排序 void merge(int l, int r) { if (l >= r) { return; } int mid = (l + r) / 2; merge(l, mid); merge(mid + 1, r); mergesort(l, mid, mid + 1, r); } int main() { IOS; cin >> n; for (int i = 0; i < n; i++) { cin >> a[i]; } merge(0, n - 1); for (int i = 0; i < n; i++) { cout << a[i]; if (i < n - 1) cout << " "; } cout << endl; return 0; } ``` 上述代码中,`merge`函数用于递归地将数组分解为子数组,`mergesort`函数用于合并两个已排序的子数组。在`main`函数中,首先读取输入的数组,然后调用`merge`函数进行排序,最后输出排序后的数组。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值