容斥原理

目录

一,容斥原理

1,原理

2,模板

3,常见应用

二,OJ实战(模板)

 力扣 920. 播放列表的数量

力扣 2928. 给小朋友们分糖果 I

力扣 2929. 给小朋友们分糖果 II

力扣 3116. 单面值组合的第 K 小金额

三,OJ实战(非模板)

CSU 2140 Rikka's function

CSU 1803 2016

UVA 10325 The Lottery

力扣 2930. 重新排列后包含指定子字符串的字符串数目


一,容斥原理

1,原理

2,模板

template<typename T=long long>
class InclusionExclusion {
public:
	T getSum(int low, int high) {
		T ans = 0, flag = 1;
		for (int i = low; i <= high; i++) {
			ans += getValue(i)*flag;
			flag *= -1;
		}
		return ans;
	}
protected:
	virtual T getValue(int key) {
		return 0;
	}
};

3,常见应用

(1)在n个球中可重复的选s次,求其中b个球至少被选中一次的情况数

公式:

f(s,n,b)=\sum _{i>=0}\binom{b}{i}(-1)^i(n-i)^s

class IECombina:public InclusionExclusion<> {
public:
	long long getSumNum(int n, int s, int b) {
		this->n = n, this->s = s, this->b = b;
		return getSum(0, b);
	}
private:
	long long getValue(int key) {
		return Multi::multiMulti(n - key, s, p) * ComNum::getValue(b, key, p) % p;
	}
	int n, s, b;
	vector<long long>comninaNum;
	int p = 1000000007;
};

(2)在n个球中可重复的选s次,每相邻k次之中不能有重复的球,求其中每个球至少被选中一次的情况数

公式:

f(n,s,k)=\frac{n!(-1)^{n-k}}{(n-k)!}\sum _{i>=0}\binom{n-k}{i}(-1)^ii^{s-k}

class IECombina2 :public InclusionExclusion<> {
public:
	long long getSumNum(int n, int s, int k) {
		this->n = n, this->s = s, this->k = k;
		long long ans = (getSum(0, n-k) % p + p) % p;
		if (n % 2 != k % 2)ans = p - ans;
		for (int i = 0; i < k; i++)ans = ans * (n - i) % p;
		return ans % p;
	}
private:
	long long getValue(int key) {
		return Multi::multiMulti(key, s - k, p) * ComNum::getValue(n - k, key, p) % p;
	}
	int n, s, k;
	int p = 1000000007;
};

(3)从1到n有多少数是v中任一数的倍数

class IECombina3 :public InclusionExclusion<> {
public:
	long long getSumNum(const vector<int>& v, long long n) { //从1到n有多少数是v中任一数的倍数
		m.clear();
		for (int i = 1; i < (1 << v.size()); i++) {
			long long lcms = 1;
			int s = 0;
			for (int j = 0; j < v.size(); j++) {
				if (i & (1 << j))s++, lcms = lcm(lcms, v[j]);
				if (lcms > n || lcms == 0)break;
			}
			m[s] += n / lcms;
		}
		return getSum(1, v.size());
	}
private:
	long long getValue(int key) {
		return m[key];
	}
	//最大公约数
	static long long gcd(long long a, long long b)
	{
		if (b == 0)return a;
		return gcd(b, a % b);
	}
	//最小公倍数
	static long long lcm(long long a, long long b)
	{
		a /= gcd(a, b);
		if (a > LLONG_MAX / b)return 0;
		return a * b;
	}
private:
	map<int, long long>m;
};

二,OJ实战(模板)

 力扣 920. 播放列表的数量

你的音乐播放器里有 n 首不同的歌,在旅途中,你计划听 goal 首歌(不一定不同,即,允许歌曲重复)。你将会按如下规则创建播放列表:

  • 每首歌 至少播放一次 。
  • 一首歌只有在其他 k 首歌播放完之后才能再次播放。

给你 ngoal 和 k ,返回可以满足要求的播放列表的数量。由于答案可能非常大,请返回对 109 + 7 取余 的结果。

示例 1:

输入:n = 3, goal = 3, k = 1
输出:6
解释:有 6 种可能的播放列表。[1, 2, 3],[1, 3, 2],[2, 1, 3],[2, 3, 1],[3, 1, 2],[3, 2, 1] 。

示例 2:

输入:n = 2, goal = 3, k = 0
输出:6
解释:有 6 种可能的播放列表。[1, 1, 2],[1, 2, 1],[2, 1, 1],[2, 2, 1],[2, 1, 2],[1, 2, 2] 。

示例 3:

输入:n = 2, goal = 3, k = 1
输出:2
解释:有 2 种可能的播放列表。[1, 2, 1],[2, 1, 2] 。

提示:

  • 0 <= k < n <= goal <= 100
class Solution {
public:
	int numMusicPlaylists(int n, int goal, int k) {
		return IECombina2().getSumNum(n, goal, k);
	}
};

力扣 2928. 给小朋友们分糖果 I

给你两个正整数 n 和 limit 。

请你将 n 颗糖果分给 3 位小朋友,确保没有任何小朋友得到超过 limit 颗糖果,请你返回满足此条件下的 总方案数 。

示例 1:

输入:n = 5, limit = 2
输出:3
解释:总共有 3 种方法分配 5 颗糖果,且每位小朋友的糖果数不超过 2 :(1, 2, 2) ,(2, 1, 2) 和 (2, 2, 1) 。

示例 2:

输入:n = 3, limit = 3
输出:10
解释:总共有 10 种方法分配 3 颗糖果,且每位小朋友的糖果数不超过 3 :(0, 0, 3) ,(0, 1, 2) ,(0, 2, 1) ,(0, 3, 0) ,(1, 0, 2) ,(1, 1, 1) ,(1, 2, 0) ,(2, 0, 1) ,(2, 1, 0) 和 (3, 0, 0) 。

提示:

  • 1 <= n <= 50
  • 1 <= limit <= 50
class Solution :public InclusionExclusion<>, public ComNum<> {
public:
	int distributeCandies(int n, int limit) {
		this->n = n, this->k = limit;
		return getSum(0, 3);
	}
	long long getValue(int key) {
		int s = n + 2 - key * (k + 1);
		return s >= 2 ? getComNum(3, key) * getComNum(s, 2) : 0;
	}
	int n, k;
};

力扣 2929. 给小朋友们分糖果 II

给你两个正整数 n 和 limit 。

请你将 n 颗糖果分给 3 位小朋友,确保没有任何小朋友得到超过 limit 颗糖果,请你返回满足此条件下的 总方案数 。

示例 1:

输入:n = 5, limit = 2
输出:3
解释:总共有 3 种方法分配 5 颗糖果,且每位小朋友的糖果数不超过 2 :(1, 2, 2) ,(2, 1, 2) 和 (2, 2, 1) 。

示例 2:

输入:n = 3, limit = 3
输出:10
解释:总共有 10 种方法分配 3 颗糖果,且每位小朋友的糖果数不超过 3 :(0, 0, 3) ,(0, 1, 2) ,(0, 2, 1) ,(0, 3, 0) ,(1, 0, 2) ,(1, 1, 1) ,(1, 2, 0) ,(2, 0, 1) ,(2, 1, 0) 和 (3, 0, 0) 。

提示:

  • 1 <= n <= 106
  • 1 <= limit <= 106

思路:

题目和2928. 给小朋友们分糖果 I是一样的,只是数字范围变了。

这里的组合数刚好超过了int又没有超过long long,所有直接求值,不能mod p,我的模板是mod p的,这里就干脆改成手动计算组合数。

class Solution :public InclusionExclusion<>, public ComNum<> {
public:
	long long distributeCandies(int n, int limit) {
		this->n = n, this->k = limit;
		return getSum(0, 3);
	}
	long long getValue(int key) {
		long long s = n + 2 - key * (k + 1);
        long long C3key = key%3?3:1;
        long long Cs2 = s*(s-1)/2;
		return s >= 2 ? C3key * Cs2 : 0;
	}
	int n, k;
};

力扣 3116. 单面值组合的第 K 小金额

给你一个整数数组 coins 表示不同面额的硬币,另给你一个整数 k 。

你有无限量的每种面额的硬币。但是,你 不能 组合使用不同面额的硬币。

返回使用这些硬币能制造的 第 kth 小 金额。

示例 1:

输入: coins = [3,6,9], k = 3

输出: 9

解释:给定的硬币可以制造以下金额:
3元硬币产生3的倍数:3, 6, 9, 12, 15等。
6元硬币产生6的倍数:6, 12, 18, 24等。
9元硬币产生9的倍数:9, 18, 27, 36等。
所有硬币合起来可以产生:3, 6, 9, 12, 15等。

示例 2:

输入:coins = [5,2], k = 7

输出:12

解释:给定的硬币可以制造以下金额:
5元硬币产生5的倍数:5, 10, 15, 20等。
2元硬币产生2的倍数:2, 4, 6, 8, 10, 12等。
所有硬币合起来可以产生:2, 4, 5, 6, 8, 10, 12, 14, 15等。

提示:

  • 1 <= coins.length <= 15
  • 1 <= coins[i] <= 25
  • 1 <= k <= 2 * 109
  • coins 包含两两不同的整数。

class Solution :public Bsearch<long long>, public IECombina3 {
public:
	long long findKthSmallest(vector<int>& coins, int k) {
		this->coins = coins, this->k = k;
		return find(1, coins[0] * this->k);
	}
private:
	bool isOk(long long x) //若isOk(x)且!isOk(y)则必有y<x
	{
		return getSumNum(coins, x) >= k;
	}
	vector<int> coins;
	long long k;
};

三,OJ实战(非模板)

CSU 2140 Rikka's function

题目:

思路:

代码:

#include<iostream>
using namespace std;
 
int p = 1000000007;
long long f[1000005];
 
long long get_mi(long long n, int k)
{
	if (k == 0)return 1;
	long long r = get_mi(n, k / 2) % p;
	r = (r*r) % p;
	if (k % 2)r = (r*n) % p;
	return r;
}
 
int main()
{
	int n, m;
	cin >> n >> m;
	f[m] = m, f[m + 1] = 1;
	for (int i = m - 1; i >= 1; i--)f[i] = f[i + 1] * i%p;
	long long s = 0, t = get_mi(f[1], p - 2) % p;
	for (int i = 0; i <= m; i++)
	{
		long long r = t;
		if (i % 2)r *= -1;
		r = r*f[i+1] % p;
		r = r*f[m-i+1] % p;
		r = r*get_mi(m - i, n) % p;
		s += r;
	}
	s = s%p + p;
	cout << s%p << endl;
	return 0;
}

CSU 1803 2016

题目:


Description

 给出正整数 n 和 m,统计满足以下条件的正整数对 (a,b) 的数量:
1. 1≤a≤n,1≤b≤m;
2. a×b 是 2016 的倍数。
Input

输入包含不超过 30 组数据。
每组数据包含两个整数 n,m (1≤n,m≤10 9).
Output
对于每组数据,输出一个整数表示满足条件的数量。
Sample Input
32 63
2016 2016
1000000000 1000000000
Sample Output
1
30576
7523146895502644

这个题目直接枚举g=gcd(a,2016)即可
2016=2*2*2*2*2*3*3*7,所以g有6*3*2=36种情况。

对于每个g,求a满足gcd(a,2016)恰好是g,求b使得2016| a*b

一,求a满足gcd(a,2016)恰好是g

即求1,2,3......n/g这些数中,有多少个和2016/g互质,用容斥原理即可

二,求b使得2016| a*b

即求1,2,3......m这些数中,有多少个是2016/g的倍数,答案就是m*g/2016

代码:
 

#include<iostream>
using namespace std;

int n, m;

long long f(int g)
{
	int nn = n / g;
	long long s = nn - (1008 % g == 0)*nn / 2 - (672 % g == 0)*nn / 3 - (288 % g == 0)*nn / 7;
	s += (336 % g == 0)*nn / 6 + (96 % g == 0)*nn / 21 + (144 % g == 0)*nn / 14 - (48 % g == 0)*nn / 42;
	long long gg = g;
	return gg*m / 2016 * s;
}

long long f7(int g)
{
	return f(g) + f(g * 7);
}

long long f5(int g)
{
	return f7(g) + f7(g * 3) + f7(g * 9);
}

int main()
{
	while (cin >> n >> m)cout << f5(1) + f5(2) + f5(4) + f5(8) + f5(16) + f5(32) << endl;
	return 0;
}

UVA 10325 The Lottery

题目:

用了状态压缩来枚举2^m种状态,用k对应状态。

注意一个细节,当r超过n时,r/n就已经是0了,就不要再继续求最小公倍数了,否则会越界,因为多个数的最小公倍数很容易超过long long的范围。

代码:

#include<iostream>
using namespace std;
 
int gcd(int a, int b)
{
	if (b)return gcd(b, a%b);
	return a;
}
 
int main()
{
	int n, m, l[15], s;
	while (cin >> n >> m)
	{
		s = 0;
		for (int i = 0; i < m; i++)cin >> l[i];
		for (int k = 0; k < (1 << m); k++)
		{
			long long r = 1, b = 0;
			for (int i = 0; i < m; i++)if (k&(1 << i))
			{
				if (r <= n)r *= l[i] / gcd(l[i], r);
				b++;
			}
			if (b % 2)s -= n/r;
			else s += n/r;
		}
		cout << s << endl;
	}
	return 0;
}

力扣 2930. 重新排列后包含指定子字符串的字符串数目

给你一个整数 n 。

如果一个字符串 s 只包含小写英文字母, 将 s 的字符重新排列后,新字符串包含 子字符串 "leet" ,那么我们称字符串 s 是一个  字符串。

比方说:

  • 字符串 "lteer" 是好字符串,因为重新排列后可以得到 "leetr" 。
  • "letl" 不是好字符串,因为无法重新排列并得到子字符串 "leet" 。

请你返回长度为 n 的好字符串  数目。

由于答案可能很大,将答案对 109 + 7 取余 后返回。

子字符串 是一个字符串中一段连续的字符序列。

示例 1:

输入:n = 4
输出:12
解释:总共有 12 个字符串重新排列后包含子字符串 "leet" :"eelt" ,"eetl" ,"elet" ,"elte" ,"etel" ,"etle" ,"leet" ,"lete" ,"ltee" ,"teel" ,"tele" 和 "tlee" 。

示例 2:

输入:n = 10
输出:83943898
解释:长度为 10 的字符串重新排列后包含子字符串 "leet" 的方案数为 526083947580 。所以答案为 526083947580 % (109 + 7) = 83943898 。

提示:

  • 1 <= n <= 105
class Solution {
public:
    int stringCount(int n) {
        int p=1000000007;
        long long mi23=Multi::multiMulti(23,n-1,p);
        long long mi24=Multi::multiMulti(24,n-1,p);
        long long mi25=Multi::multiMulti(25,n-1,p);
        long long mi26=Multi::multiMulti(26,n-1,p);
        long long ans=mi25-mi24*2+mi23;
        mi23*=23,mi24*=24,mi25*=25,mi26*=26;
        return ((mi26-mi25*3+mi24*3-mi23-n*ans)%p+p)%p;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值