暴力枚举之利用进制转换进行枚举

        最近在学习暴力枚举的过程中读到洛谷辅导书中的一篇文章,从中我学会了一种新的思路去表达枚举情况,下面我将这篇文章大致内容分享给大家。(原创:《洛谷-深入浅出程序设计竞赛基础篇》)

        首先我们引入一个例题,选数(洛谷P1036),从n(n<=20)个整数中任选k个整数相加,求一共有多少种选择情况可以使和为质数。

        拿到这个题的时候,我脑子里第一浮现出来的就是靠暴力的枚举和循环找出所有情况,同时一一判断这些情况是否符合题意,然而问题很明显也随之而来。我该如何一一列举每个情况呢?因为是从n个数中选出k个数,我从来没有学过如何类似的算法,我立刻陷入了僵局,我无法想到如何处理这些数字,好让枚举出所有情况的同时做到不遗漏又不重复。

        好在这本书中的这篇文章给了我一个新的思路,利用十进制和二进制一一对应的关系来代替每一种情况,这是什么意思呢?首先让我们思考一下,假如我们只有5个数1~5,并从中选出几个数进行组合,那么我们可以用集合A={1,2,3,4,5}代表这些数,同时我们用“1”代表我们选了这个数,用“0”表示我们没有选这个数,那么举几个例子:

        可以看到,我们用A1-4这四种情况表示我们分别选取表格中1~5几个数字组成的情况,其中我们将每个数字选择的情况用1或者0代替,将这些1,0连在一起,如表中的11101,11001等,将它们看作二进制,发现每种情况都有一个十进制数字与之对应。就好比数学中的数形结合思想一般,在数学中,我们将数轴上每一个点和每一个数字一一对应。而在这个算法中,我们将每个10进制数字与每一个2进制数字一一对应起来,于此同时这些10进制的数字就恰好表示了1~5这几个数字的选择情况,可以想到我们可以以此来进行统计。

        现在我们已经想到了可以用10进制表示每一种情况,那么又该如何解决这个实际问题呢?在那之前,我们还要回想一下很少被我们用到的几个运算符:“| & ^ << ”等。在集合中,例如上图给出的例子A2和A3,它们的并集就是A1,我们发现A1的每一位都是A2orA3的结果,那不就是我们的A2|A3所得的结果吗?因为这些运算符在运算时,会自动将十进制转换为二进制,在每一位进行相应运算,所有我们可以放心地使用它们,不需要再自己考虑十进制和二进制的转化关系。同理我们还可以想到交集,如A3=A1&A4;补集,如A2对于A1(全集)的补集就是A3,A3=A1^A2。等等通过这些运算符和集合联系起来的操作。

        正是想到了以上这些,我们可以利用二进制和十进制的一一对应关系解决这个问题。同时书中也介绍了一个函数__builtin_popcount(),它可以直接返回一个数二进制下的1的个数。利用以上技巧和知识,可以得到以下代码:

#include "iostream"
#include "cstdio"
using namespace std;
int a[30];
bool check(int x) {
	for (int i = 2; i * i <= x; i++)
		if (x % i == 0)return 0;
	return 1;
}
int main()
{
	int n, k, ans = 0;
	cin >> n >> k;
	for (int i = 0; i < n; i++)
		cin >> a[i];
	int U = 1 << n;
	for(int s=0;s<U;s++)
		if (__builtin_popcount(s) == k) {
			int sum = 0;
			for (int i = 0; i < n; i++)
				if (s & (1 << i))sum += a[i];
			if (check(sum))ans++;
		}
	cout << ans;
	return 0;
}

        可以看到,题目中规定了一个全集U,令全集的值为U-1,让s从0开始自增,模拟枚举所有子集[0.U),如果枚举情况中1的个数和题目给出的k相等,就进一步进行判断,如果第i个元素在s中,就把这个值加入到sum中。

        综上所述,这个方法可以很好地解决这个问题,又可以没有遗漏和重复的情况。利用这个方式,我们还可以解决一个类似的问题。例如洛谷p1157,组合的输出。从自然数1-n中任选r个数作为一个组合,并输出所有的组合情况,每个组合数字从小到大输出。但是据题意可知,这个题目我们可以倒着枚举,从而满足题意要求,代码如下:

#include "stdio.h"
#include "iostream"
using namespace std;
int a[30];
int main()
{
	int n, r;
	cin >> n >> r;
	for (int s = (1 << n) - 1; s >= 0; s--)
	{
		int cnt = 0;
		for (int i = 0; i < n; i++)
			if (s & (1 << i))
				a[cnt++] = i;
		if (cnt == r) {
			for (int i = r - 1; i >= 0; i--)
				cout << n - a[i];
			puts(" ");
		}
	}
	return 0;
}

        总之,通过这个方法,我们开拓了新的思路,有了新的方向解决相关问题,这些知识属实帮了我大忙。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值