暴力枚举-例题篇

  • 循环枚举

  1. 例10-1:(洛谷P2241)统计方形加强版

法1:

对于每一个点,其对应的正方形个数sqr为min(n-x,y)+min(x,y)+min(x,m-y)+min(n-x,m-y)。

长方形由于该点的对角点不能和该点同行或同列,则(m+1-1)*(n+1-1)-正方形个数sqr即为长方形的个数。

由于n,m<=5000,我们可以借助计算机去帮我们确定数据范围,线假设为int,可得:

#include 
#include 
using namespace std;
int main() {
	int n, m;
	cin >> n >> m;
	int sqr;//正方形
	int sum1=0;//正方形的和
	int sum2 = 0;//长方形的和
	for (int x = 0; x <= n; x++) {
		for (int y = 0; y <= m; y++) {
			sqr = min(n - x, y) + min(y, x) + min(x, m - y) + min(n - x, m - y);
			sum1 += sqr;
			sum2 += n * m -sqr;
		}
	}
	printf("%d %d", sum1 / 4, sum2 / 4);
	return 0;
}

此时输入n=5000,m=5000,可判断溢出。

因此更改数据类型为long long。

#include 
#include 
using namespace std;
int main() {
	int n, m;
	cin >> n >> m;
	long long sqr;//正方形
	long long sum1=0;//正方形的和
	long long sum2 = 0;//长方形的和
	for (int x = 0; x <= n; x++) {
		for (int y = 0; y <= m; y++) {
			sqr = min(n - x, y) + min(y, x) + min(x, m - y) + min(n - x, m - y);
			sum1 += sqr;
			sum2 += n * m -sqr;
		}
	}
	printf("%lld %lld", sum1 / 4, sum2 / 4);
	return 0;
}

此时通过。

法2:

法1有重复的情况,并不够简洁高效。

下面尝试去除重复情况:
了解关键所在:重复情况是因为我们对于枚举的一个点向四周进行了延伸,这是导致重复的本质。

如果我们只对枚举的一点的左上角进行延伸,这个问题就不会存在。

此时对于所枚举的一点,有:

正方形个数sqr=min(x,y);

长方形个数为(x+1-1)*(y+1-1)-sqr;

由此可以得到代码:

#include 
#include 
using namespace std;
int main() {
	int n, m;
	cin >> n >> m;
	long long sqr;//正方形
	long long sum1=0;//正方形的和
	long long sum2 = 0;//长方形的和
	for (int x = 0; x <= n; x++) {
		for (int y = 0; y <= m; y++) {
			sqr = min(x, y);
			sum1 += sqr;
			sum2 += x*y-sqr;
		}
	}
	printf("%lld %lld", sum1 , sum2);
	return 0;
}
法3:

枚举其他要素:边长

如果我们枚举边长a*b,那么

1.a==b

正方形。运用排列组合知识(分步乘法计数原理),可得其个数自增(n-a+1)*(m-b+1)

2.a!=b

长方形。同理其个数自增(n-a+1)*(m-b+1);

可得代码:

#include 
#include 
using namespace std;
int main() {
	int n, m;
	cin >> n >> m;
	long long sum1=0;//正方形的和
	long long sum2 = 0;//长方形的和
	for (int a = 1; a <= n; a++) {
		for (int b = 1; b <= m; b++) {
			if (a == b) {
				sum1 += (n - a + 1) * (m- b + 1);
			}
			else {
				sum2+= (n - a + 1) * (m - b + 1);
			}
		}
	}
	printf("%lld %lld", sum1 , sum2);
	return 0;
}

此时边长从1开始算。

法4:

继续减少枚举量。

在法3中,我们知道a==b的时候,正方形的个数。

如果我们只算正方形的个数,由于a==b,我们可以只写一层循环:

	for (int a = 1; a <= min(n,m); a++) {
		sum1 += (n - a + 1) * (m- a + 1);
	}

我们又知道:正方形的个数+长方形的个数=所有矩形的个数

如果我们能轻易地求出所有矩形的个数,那么问题迎刃而解了。

利用排列组合知识(分步乘法计数原理),对于两个对应的横线(或竖线)有总个数枚举有1+2+3+......+n(1+2+3+......+m),我们可以求得所有矩形的个数为=1/2*n(n+1)  *  1/2*m(m+1)。

则有:

#include 
#include 
using namespace std;
int main() {
	int n, m;
	cin >> n >> m;
	long long sum1=0;//正方形的和
	long long sum2 = 0;//长方形的和
	for (int a = 1; a <= min(m,n); a++) {
		sum1 += (n - a + 1) * (m- a + 1);
	}
	sum2 =n * (n + 1) * m * (m + 1)/4-sum1;
	printf("%lld %lld", sum1 , sum2);
	return 0;
}

但是n=m=5000时此段代码又发生了溢出;

这就造成了一个典型的错误:int*int型在等式右边仍为int型再被强制类型转换为long long已经无力回天。

应该把m,n都设置为long long型才可以。

#include 
#include 
using namespace std;
int main() {
	long long n, m;
	cin >> n >> m;
	long long sum1=0;//正方形的和
	long long sum2 = 0;//长方形的和
	for (int a = 1; a <= min(m,n); a++) {
		sum1 += (n - a + 1) * (m- a + 1);
	}
	sum2 =n * (n + 1) * m * (m + 1)/4-sum1;
	printf("%lld %lld", sum1 , sum2);
	return 0;
}
启发:

本题通过不断改进算法,更改枚举的要素和减少枚举量,达到了优秀的复杂度和简洁易懂的代码风格。我们今后也应当尝试切换多种枚举方式来获得优秀的解题效果。

例10-2:(洛谷P2089)烤鸡

法1:

易得:

#include 
using namespace std;
int main() {
	int n;
	cin >> n;
	int flag = 0,ans=0;
	for (int i1 = 1; i1 <= 3; i1++) {
		for (int i2 = 1; i2<= 3; i2++) {
			for (int i3 = 1; i3 <= 3; i3++) {
				for (int i4 = 1; i4 <= 3; i4++) {
					for(int i5 = 1; i5 <= 3; i5++) {
						for (int i6 = 1; i6 <= 3; i6++) {
							for (int i7 = 1; i7 <= 3; i7++) {
								for (int i8 = 1; i8 <= 3; i8++) {
									for (int i9 = 1; i9<= 3; i9++) {
										for (int i10 = 1; i10<= 3; i10++) {
											if (i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8 + i9 + i10 == n) {
												ans ++;
												flag = 1;
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}
	if (flag == 0) cout << 0;
	else {
		cout << ans<
宏的构造语句功能

为了简化代码,我们使用宏的另外一种用法:构造语句。

#define rep(i,a,b) for(int i=a;i<=b;i++)

这样可以简化代码:

#include 
using namespace std;
#define rep(i,a,b) for(int i=a;i<=b;i++)
int main() {
	int n;
	cin >> n;
	int flag = 0,ans=0;
	rep(i1,1,3) rep(i2,1,3) rep(i3,1,3) rep(i4,1,3)rep(i5,1,3)rep(i6,1,3)
		rep(i7,1,3)rep(i8,1,3)rep(i9,1,3)rep(i10,1,3)
		if (i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8 + i9 + i10 == n) {
			ans++;
			flag = 1;
		}
	if (flag == 0) cout << 0;
	else {
		cout << ans<

注意使用宏的构造语句功能时,宏定义只会做简单的字符串替换。

如#define prod(a,b) a*b

那么

prod(a+b,c)编译器会理解为a+b*c,并非想要的(a+b)*c

所以在定义宏时要请勤加括号,如:

#define prod(a,b)  (a)*(b)

这样可以有效避免出现运算优先级的bug。

法2:

最严格的优化思路,请移步P145页

例10-3:(洛谷P1618)三连击升级版

明确一点:

如果使用9重循环去枚举这9个数字,第一个循环从1到9,第二个,第三个依此类推......

这样需要9的9次方次,1s很难完成这样的任务。需要寻求他法。

法1:

我们不妨先枚举一个3位数,再根据比例关系确定其他三(n)位数是否符合要求。

如果分解得到的数据是1,2,3,4,5,6,7,8,9,那么他们各自都应该e[i]为1,一旦有一个为0,就说明check返回false。

#include 
using namespace std;
#include 
#include 
#include 
int e[10] ;
void go(int d) {
	e[d % 10] = 1;
	e[d / 10 % 10] = 1;
	e[d / 100] = 1;
}
bool check(int a,int b,int c) {
	memset(e, 0, sizeof(e));
	if (b > 987 || c > 987) return 0;
	go(a); go(b); go(c);
	for (int i = 1; i <= 9; i++) {
		if (!e[i]) {
			return 0;
		}
	}
	return 1;
}
int main() {
	long long A, B, C, flag=0,a,b,c;
	cin >> A >> B >> C;
	if (A != 0&&B!=0&&C!=0) {
		for (int i = 123; i <= 987; i++) {
			a = i;
			if (a * B % A || a * C % A) continue;//如果除不尽直接跳入下一循环
			b = a * B / A;
			c = a * C / A;
			if (check(a, b, c)) {
				cout << a << " " << b << " " << c << endl;
				flag++;
			}
		}
		if (!flag) {
			cout << "No!!!";
		}
	}
	else {
		cout << "No!!!";
	}
	
	return 0;
}

子集枚举

从一个有n个数字的集合中挑选出一些数字(也就是子集),然后判断该子集是否满足某个性质。

集合枚举即为从一个集合中找出他的所有子集。n个元素的集合共有2的n次方个子集(包括全集和空集)。

例10-4:(洛谷P1036)选数

预备知识:
A中元素 1 2 3 4 5 二进制 对应十进制
在A1中出现情况 1 0 1 1 1 11101 a1=29
在A2中出现情况 1 0 0 1 1 11001 a2=25
在A3中出现情况 0 0 1 0 0 00100 a3=4
在A4中出现情况 0 1 1 0 0 00110 a4=6

由此可知,

下面有3种特殊的集合对应的十进制数:

1.仅包含第i个元素的集合:1<<(i-1);

2.全集:(1<

3.空集:0

子集和二进制数的关系:

1.两个子集的并集→a1=a2|a3

2.两个子集的交集→a3=a1&a4

3.包含:A1包含A2→((a1|a2)==a1&&(a1&a2)==a2)

4.属于:判断1个元素是否属于一个集合→判断该单元素集是否是这个集合的子集→两集合取交,若不为空集,则命题为真,如第三个元素是否属于A1,可写成:1<<(3-1)&a1

5.补集:A2的补集A3可以表示为a^a2(^为C++里的异或运算)

内建函数__buildin_popcount()(不常用)

统计二进制中1的个数,其直接返回一个二进制下1的个数

template 
unsigned int __builtin_popcount(Dtype u)
{
	u = (u & 0x55555555) + ((u >> 1) & 0x55555555);
	u = (u & 0x33333333) + ((u >> 2) & 0x33333333);
	u = (u & 0x0F0F0F0F) + ((u >> 4) & 0x0F0F0F0F);
	u = (u & 0x00FF00FF) + ((u >> 8) & 0x00FF00FF);
	u = (u & 0x0000FFFF) + ((u >> 16) & 0x0000FFFF);
	return u;
}//wishchin!!!  
__buildin_popcount()的代替函数bitcount()

bitcount()是自己实现的函数,比逐步遍历来的快。

int bitcount(unsigned int n)
{
	int count = 0;
	while (n) {
		count++;
		n &= (n - 1);
	}
	return count;
}

每一次n&=(n-1)都可以去掉其二进制里的一个1。

正式应用:
int bitcount(unsigned int n)
{
	int count = 0;
	while (n) {
		count++;
		n &= (n - 1);
	}
	return count;
}
#include 
#include 
using namespace std;
bool check(int a) {
	if (a != 0 && a != 1) {
		for (int i = 2; i * i <= a; i++) {
			if (a % i == 0) return 0;
		}
		return 1;
	}
	else {
		return 0;
	}
}
int main() {
	int n, k;
	int ans = 0;
	int a[25];
	cin >> n >> k;
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}
	int U = 1 << n;//U-1为全集
	for (int S = 0; S < U; S++) {//枚举子集S
		if (bitcount(S) == k) {
			int sum = 0;
			for (int i = 0; i < n; i++) {
				if (1 << i & S) sum += a[i];
			}
			if (check(sum)) {
				ans++;
			}
		}
	}
	cout << ans;
	return 0;
}

虽然总是用二进制数来描述这些表示集合的数,但是在实际操作中会把这些二进制数当做一个整体存储为单独的数字(可以认为存下了二进制数对应的十进制数),而不会把它的每一位分别存储,也不会区别对待它们与普通变量。

例10-5:(洛谷P1157)组合的输出

如何实现字典序:


为了让数字依次递增,1尽量在左边先出现,可以设置U从全集递减,高位到地位为元素1到n,这样我们就可以实现字典序。

比如:

a1=1 a2=2 a3=3 a4=4 a5=5 十进制数
正常 0 0 1 1 1
字典序 1 1 1 0 0

我们先枚举到11100,然后用一个数组依次存下5、 4、 3,要输出1、 2、 3,只需输出n+1-i即可。

则得:

#include 
#include 
using namespace std;
int bitcount(int n) {
	int count = 0;
	while (n) {
		count++;
		n &= (n - 1);
	}
	return count;
}
int main() {
	int n, r;
	int ans[30];
	cin >> n >> r;
	int U = 1 << n;//U-1为全集
	for (int S = U-1; S >=0; S--) {
		int cnt = 0;
		if (bitcount(S) == r) {
			for (int i = n; i >=1; i--) {
				if (1 << (i - 1) & S) {
					ans[++cnt] = i;
				}
			}
			for (int i = 1; i<=cnt; i++) {
				printf("%3d", n+1-ans[i]);
			}
			printf("\n");
		}

	}
	return 0;
}
总结:

枚举子集的算法时间复杂度是o(2的n次方),一般情况下1s可以枚举20-30个元素集合的子集,如果枚举对顺序有要求,就要确定枚举的方向和每一位代表什么元素。

排列枚举:

例10-3:(洛谷P1618)三连击升级版

 bool  next_permutation()函数

其为algorithm标准库中的一个标准函数,可以在表示[start,end)内存的数组中产生严格的下一个字典序排列。

当当前序列不存在下一个排列时,函数返回false,否则返回true。

next_permutation(num,num+n)函数是对数组num中的前n个元素进行全排列,并同时改变num数组的值。

另外,需要强调的是,next_permutation()在使用前需要对欲排列数组按升序排序,否则只能找出该序列之后的全排列数。

应用:
#include 
#include 
using namespace std;
int main() {
	int a[20];
	for (int i = 1; i <= 9; i++) {
		a[i] = i;
	}
	long long num1, num2, num3, A, B, C, flag = 0;
	cin>>A>>B>>C;
	do {
		num1 = a[1] * 100 + a[2] * 10 + a[3];
		num2 = a[4] * 100 + a[5] * 10 + a[6];
		num3 = a[7] * 100 + a[8] * 10 + a[9];
		if(num1*B==num2*A&&num2*C==num3*B)
		flag=1,cout << num1 <<' ' << num2<<' ' << num3<

例10-6:(洛谷P1706)全排列问题

#include 
#include 
#include 
using namespace std;
int main() {
	int a[15];
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		a[i] = i;
	}
	do {
		for (int i = 1; i <= n; i++) {
			printf("%5d", a[i]);
		}
		printf("\n");
	} while (next_permutation(a+1,a+n+1));
	return 0;
}

例10-7:(洛谷P1088)火星人

#include 
using namespace std;
#include 
int a[10010];
int main() {
	int n,m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	for(int i=1;i<=m;i++)
	next_permutation(a + 1, a + n + 1);
	for (int i = 1; i <= n; i++) {
		if (i != n)
			cout << a[i] << " ";
		else
			cout << a[i];
	}
		
	return 0;
}

枚举所有全排列的算法时间复杂度是O(n!),一般情况下1s很难枚举超过11个元素的全排列。

你可能感兴趣的:(数据结构与算法,算法)