深入浅出程序设计竞赛(洛谷基础篇) 第十章 暴力枚举

例10-1 统计方形加强版
// 法一:枚举平面内的任意一点,统计以这个点为顶点的正方形和长方形数量
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int main()
{
	LL n,m,squ = 0,rec=0;
	scanf("%lld%lld",&n,&m);
	for(LL x = 0;x<=n;x++)
		for(LL y = 0;y<=n;y++){
			LL tmp = min(x,y) + min(y,n-x) + min(n-x,m-y) + min(m-y,x);
			squ += tmp;
			rec += n * m - tmp;
		}
	printf("%lld %lld",squ / 4,rec / 4); // 每个正方形或者长方形会被周围围成的四个顶点都枚举到,所以我们要除掉
	return 0;
}

// 法二:只枚举右下角(或者左上角)的顶点,这样可以保证没有重复的情况出现
for(LL x = 0;x<=n;x++)
	for(LL y = 0;y<=m;y++){
		LL tmp  = min(x,y);
		squ+=tmp;
		rec += x * y -tmp;
	}

// 法三:枚举方形的边长,通过递推的关系我们得到数学表达式:nxm中axb的小矩形的个数为 (n-a)*(m-b)
for(LL a = 0;a<n;a++)
	for(LL b = 0;b<n;b++){
		if(a == b)
			squ += (n-a) * (m-b);
		else
			rec += (n-a) * (m-b); 
	}

// 法四:枚举所有可能的高度和宽度:我们这里是基于法三进行的再优化,直接把复杂度从O(n^2)降到了O(n)
// 选择两条不同的横线:C(n+1,2)=n(n+1)/2
// 选择两条不同的竖线:C(m+1,2)=m(m+1)/2
// 每对横线 × 每对竖线 = 一个矩形 总矩形数公式为 n(n+1)/2 * m(m+1)/2

for(LL a = 0;a<=min(m,n);a++)
	squ+=(n-a)*(m-a);
rec = n*(n+1)*m*(m+1) / 4 -squ;
printf("%lld %lld",squ,rec);

例10-2 烤鸡
// 法一:纯粹的暴力枚举

#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i = a;i<=b;i++)

int main()
{
	int n ,ans = 0,cnt = 10;
	scanf("%d",&n);
	rep(a,1,3) rep(b,1,3) rep(c,1,3) rep(d,1,3) rep(e,1,3) rep(f,1,3) rep(g,1,3) rep(h,1,3) rep(i,1,3) rep(j,1,3)
		if(a+b+c+d+e+f+g+h+i+j == n)
			ans++
	printf("%d\n"ans);
	rep(a,1,3) rep(b,1,3) rep(c,1,3) rep(d,1,3) rep(e,1,3) rep(f,1,3) rep(g,1,3) rep(h,1,3) rep(i,1,3) rep(j,1,3)
		if(a+b+c+d+e+f+g+h+i+j == n)
			printf("%d %d %d %d %d %d %d %d %d %d\n",a,b,c,d,e,f,g,h,i,j);
	return 0;
}
// 法二:剪枝优化,我们发现,如果前面几个的配料总重量超过了n,那么显然后续就没有枚举的必要了,针对这个思路,我们进行优化: 对于e 限制范围为 1<= e <=3 ,则会有 n-15-a-b-c-d <= e <= n-5-a-b-c-d,使用类似的思路重写循环即可

#include <bits/stdc++.h>
using namespace std;
#define rap(i,a,b) for(int i = max(1,a);i<=min(3,b);i++)

int main()
{
	int n = 0,ans = 0,cnt = 10;
	scanf("%d",&n);
	rap(a,n-27,n-5) rap(b,n-27-a,n-5-a) rap(c,n-27-a-b,n-5-a-b) rap(d,n-27-a-b-c,n-5-a-b-c) rap(e,n-27-a-b-c-d,n-5-a-b-c-d) rap(f,n-27-a-b-c-d-e,n-5-a-b-c-d-e) rap(g,n-27-a-b-c-d-e-f,n-5-a-b-c-d-e-f) rap(h,n-27-a-b-c-d-e-f-g,n-5-a-b-c-d-e-f-g) rap(i,n-27-a-b-c-d-e-f-g-h,n-5-a-b-c-d-e-f-g-h) rap(j,n-27-a-b-c-d-e-f-g-h-i,n-5-a-b-c-d-e-f-g-h-i)
	{
		if(a+b+c+d+e+f+g+h+i+j == n)
		{
			ans++;
		}
	}
	cout << ans\n << endl;
	rap(a,n-27,n-5) rap(b,n-27-a,n-5-a) rap(c,n-27-a-b,n-5-a-b) rap(d,n-27-a-b-c,n-5-a-b-c) rap(e,n-27-a-b-c-d,n-5-a-b-c-d) rap(f,n-27-a-b-c-d-e,n-5-a-b-c-d-e) rap(g,n-27-a-b-c-d-e-f,n-5-a-b-c-d-e-f) rap(h,n-27-a-b-c-d-e-f-g,n-5-a-b-c-d-e-f-g) rap(i,n-27-a-b-c-d-e-f-g-h,n-5-a-b-c-d-e-f-g-h) rap(j,n-27-a-b-c-d-e-f-g-h-i,n-5-a-b-c-d-e-f-g-h-i)
	{
		if(a+b+c+d+e+f+g+h+i+j == n)
		{
			cout << a << " " << b << " " << c << " " << d << " " << e << " " << f << " " << g << " " << h << " " << i << " " << j << endl;
		}
	}

例10-3 三连击升级版
// 我们只需要枚举一个三位数,就可以根据题目的比例关系枚举出另外两个三位数
#include <bits/stdc++>
using namespace std;
int b[10]; // 用来标记数字1-9是否出现
void nihao(int x){  // 把三位数的各个位数分解出来
	b[x%10] = 1;
	b[x/10 %10] = 1;
	b[x/100] = 1;
}

bool check(int x,int y,int z) //用于判断 x, y, z 三个数的数字是否刚好能组成 1~9 每个数字一次
{
	memset(b,0,sizeof(b));
	if(y>999 || z>999) return 0;
	nihao(x),nihao(y),nihao(z);
	for(int i = 1;i<=9;i++)
		if(!b[i]) return 0; //  // 如果有数字没出现,则不是满足条件的解
	return 1;
}

int main()
{
	long long A,B,C,x,y,z,cnt = 0;
	cin >> A >> B >> C;
	for(x = 123;x<=987;x++)
	{
		if(x*B%A || x*C%A) continue; // 如果有余数说明x*B/A不是整数
		y = x*B/A,z = x*C/A; // x和z都是整数
		if(check(x,y,z))
			printf("%lld %lld %lld\n",x,y,z) ,cnt++;
	}
	if(!cnt) puts("No!!!");
	return 0;
}
例10-4 选数
前置知识

一共有 5 个元素,则仅包含第 i 个 (1 <= i <= 5) 元素的集合的数字可以使用位移运算符进行构造,写为 1 << (i - 1);而包含所有元素的全集可以使用 a = (1 << n) - 1 来表示。

  • 这里的元素编号是 从 1 开始编号的:1, 2, 3, 4, 5
  • 但位运算中,位是从 0 开始计数的:第 0 位表示元素 1,第 1 位表示元素 2,依此类推

示例:

i1 << (i - 1)二进制表示表示的集合
11 << 0 = 100001{1}
21 << 1 = 200010{2}
31 << 2 = 400100{3}
41 << 3 = 801000{4}
51 << 4 = 1610000{5}
6(1 << 4) -1 = 1501111{1,2,3,4}

_builtin_popcount(S) 表示查找S集合中1的个数(集合二进制代码形式中的1)

1.并:a1 = a2|a3

2.或:a1 = a2&a3

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

4.属于: 1<<(i-1)&a1 左移i位进行对比,命题为真则属于,反之不然

// 法一:dfs
#include <bits/stdc++.h>
using namespace std;
int n,k,arr[25];
long long ans;

bool isPrime(int x)
{
	if(x == 1) return 0;
	for(int i = 2;i<=sqrt(x);i++)
	{
		if(x%i == 0) return 0;
	}
	return 1;
}

void dfs(int m,int sum,int startx) // m表示选择了几个数,sum表示当前的和,startx表示升序排列,避免重复
{
	if(m == k){
		if(isPrime(sum))
			ans++;
		return ;
	}
	for(int i = startx;i<n;i++)
		dfs(m+1,sum+arr[i],i+1); // 进行递归
		//步数要加一,和也要加
		//升序起始值要变成i+1,以免算重
	return ;
}

int main()
{
	int cnt = 0;
	cin >> n >> k;
	for(int i = 0;i<n;i++)
		cin >> arr[i];
	dfs(0,0,0);
	cout << ans << endl;
	return 0;
}

// 法二:位运算
#include <bits/stdc++.h>
using namespace std;
int a[30];

bool isPrime(int x)
{
	if(x == 1) return 0;
	for(int i = 2;i<=sqrt(x);i++)
	{
		if(x%i == 0) return 0;
	}
	return 1;
}

int main()
{
	int n,k,ans = 0;
	scanf("%d %d",&n,&k);
	for(int i = 0;i<n;i++) scanf("%d",&a[i]);
	int U = 1<<n; //U-1即为全集
	for(int S = 0;S<U;S++) // 枚举所有子集[0,U-1]
		if(_builtin_popcount(S) == k) { // 找到k元子集
			int sum = 0;
			for(int i = 0;i<n;i++)
				if(S&(1<<i)) sum+=a[i]; // 如果第i个元素在s中
			if(check(sum)) ans++;
		}
	printf("%d",ans);
	return 0;
}
例10-5 组合的输出
#include <bits/stdc++.h>
using namespace std;
int a[30];
int main()
{
	int n,r;
	cin >> n >> r;
	for(int S = (1<<n)-1;S>=0;S--){ // 从全集枚举到0
		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--)
				printf("%3d",n-a[i]); // 由于使用高位表示1,所以需要反过来输出
			puts("");
		}
	}
	return 0;
}
例10-3(重现) 三连击升级版
前置知识

next_permutation(start,end) 表示将[start,end]的数组空间进行多次排列,直至排列被举尽

注意,如果文件头使用的不是万能头要加上< algorithm >这个文件头

我们使用排列的方法再来解决这道题

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int a[10];

int main()
{
	long long A,B,C,x,y,z,cnt = 0;
	cin >> A >> B >> C;
	for(int i = 1;i<=9;i++)
		a[i] = i;
	do{
		x = a[1] * 100 + a[2] * 10 + a[3];
		y = a[4] * 100 + a[5] * 10 + a[6];
		z = a[7] * 100 + a[8] * 10 + a[9];
	}
	while(next_permutation(a+1,a+10)); // 初始先对123456789进行判断,后面进行全排列(比如下一个是132456789)
	if(!cnt) puts("No!!!");
	return 0;
}
例10-6 全排列问题
#include <bits/stdc++.h>
using namespace std;
int a[10],n;

int main()
{
	cin >> n;
	for(int i = 1;i<=n;i++)
		a[i] = i;
	do{
		for(int i = 1;i<=n;i++)
			printf("%5d",a[i]);
		puts(" ");
	}while(next_permutation(a+1,a+n+1));
	return 0
}

例10-7 火星人
#include <bits/stdc++.h>
using namespace std;

int a[10010],n,m;

int main()
{
	cin >> n >> m;
	for(int i = 1;i<=n;i++)
		cin >> a[i];
	for(;m--;)
		next_permutation(a+1,a+n+1);
	for(int i = 1;i<=n;i++)
		cout << a[i] << " ";
	return 0;
}

习题10-1 涂国旗
//法一:暴力枚举--分块
#include <bits/stdc++.h>
using namespace std;
char a[55][55];
int n,m,mi = 10000000;

int main()
{
	int i,j,k,g,ans;
	cin >> n >> m;
	for(int i = 1;i<=n;i++)
		for(int j = 1;j<=m;j++)
			cin >> a[i][j];
			
	for(i = 1;i<=n-1;i++) // 白色下面还有蓝色和红色,所以i(白和蓝的边界)枚举到(n-1)
		for(int j = i+1;j<=n-1;j++) // j(蓝与红的边界)至少要比i大1,同理要枚举到(n-1),这样可以减少枚举次数
			{
				ans = 0; // 初始化计数器
				// 壮观的枚举三个区域
				for(k = 1;k<=i;k++)
						for(g = 1;g<=m;g++)if(a[k][g] !='W') ans++;
				for(k = 1;k<=i;k++)
						for(g = 1;g<=m;g++)if(a[k][g] !='B') ans++;		
				for(k = 1;k<=i;k++)
						for(g = 1;g<=m;g++)if(a[k][g] !='R') ans++;
				mi = min(ans,mi);
			}
	cout << mi << endl;
}

// 法二:暴力枚举--多数组
// w,b,r三个数组分别记录前i行白蓝红 异的 数量
#include <bits/stdc++.h>
using namespace std;

int n,m,ans = 0x7fffffff,w[51],b[51],r[51];
string s;
int check(char c){
	int tot = 0;
	for(int i = 0;i<m;++i)
		if(s[i]!=c) ++tot;
	return tot; // 实现统计该行中元素颜色不同的个数
}

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		cin>>s;
		w[i]=w[i-1]+check('W');
		b[i]=b[i-1]+check('B');
		r[i]=r[i-1]+check('R');
	}
	for(int i=1;i<n-1;++i)
		for(int j=i+1;j<n;++j)
			ans=min(ans,w[i]+b[j]-b[i]+r[n]-r[j]);
	cout<<ans;
	return 0;
}
习题10-2 First step
#include <bits/stdc++.h>
using namespace std;
int Line,Column,Should;
char map[101][101];

int main()
{
	int i,j,k;
	cin >> Line >> Column >> Should; // 输入行,列,k
	for(i = 1;i<=Line;i++)
		for(j = 1;j<=Column;j++)
			cin >> map[i][j];
	int Sum = 0;
	for(i = 1;i<=Line;i++)
		for(j = 1;j<=Column;j++) 
		{
			//从(i,j)开始,分别横竖着看,看是否有k个空地
			bool Flag = true; // 假设有解
			for(k = 0;k<Should;k++) // 横着看
			{
				int Now;
				Now = i+k;
				if(map[Now][j] == '#'||Now>Line) // 如果越界或者撞墙,则不行
				{
					Flag = false;
					break;
				}
			}
			if(Flag)
			{
				Sum++;
			}
			
			Flag = true;
			for(k=0;k<Should;k++)//竖着看
			{
				int Now;					
				Now=j+k;
				if(Map[i][Now]=='#'||Now>Column)//如果越界或是撞墙,那么不行
				{
					Flag=false;
					break;
				}
			}
			if(Flag&&Should!=1)//当K=Should=1时,两种情况仅算一种
			{
				Sum++;
			}
		}

	cout<<Sum<<endl;
	return 0;
}
			
习题10-3 回文质数
#include <iostream>
using namespace std;

//判断是否为质数的函数
bool is_prime(int n) {
    if (n <= 2) return false;
    if (n % 2 == 0) return false;  
    for (int i = 3; i * i <= n; i += 2) {
        if (n % i == 0) return false;
    }
    return true;
}

int main()
{
	int a,b;
	cin >> a >> b;
	if(a<=5 && b>=5) cout << 5 << endl;
	if(a<=7 && b>=7) cout << 7 << endl;
	if(a<=11 && b>=11) cout << 11 << endl;

	for(int d1 = 1;d1<=9;d1++)
	{
		for(int d2 = 0;d2<=9;d2++)
		{
			int num = d1*100 + d2*10 + d1;
			if(num < a) continue;
			if(num > b) return 0;
			if(is_prime(num))
				cout << num << endl;
		}
	}

	for(int d1 = 1;d1<=9;d1++)
	{
		for(int d2 = 0;d2<=9;d2++)
		{
			for(int d3 = 0;d3<=9;d3++)
			{
				int num = d1*10000 + d2 *1000 + d3*100 + d2*10 + d1;
				if(num < a) continue;
				if(num > b) return 0;
				if(is_prime(num))
					cout << num << endl;
			}
		}
	}


	for(int d1 = 1;d1<=9;d1++)
	{
		for(int d2 = 0;d2<=9;d2++)
		{
			for(int d3 = 0;d3<=9;d3++)
			{
				for(int d4 = 0;d4<=9;d4++)
				{
					int num = d1*1000000 + d2 *100000 + d3*10000+ d4*1000 +d3*100 + d2*10 + d1;
					if(num < a) continue;
					if(num > b) return 0;
					if(is_prime(num))
						cout << num << endl;
				}
			}
		}
	}

	return 0;
}

习题10-4 火柴棒等式
#include <bits/stdc++.h>
using namespace std;
int a[]={6, 2, 5, 5, 4, 5, 6, 3, 7, 6}, n, ans;
int f (int x) { // 函数 f(x) 计算数字 x 的每一位的对应值之和。
	if (x<10)
		return a[x];
	return f(x/10)+a[x%10];
}
int main () {
	cin >> n;
	for (int i=0; i<=1000; ++i)
		for (int j=0; j<=1000; ++j)
			ans+=(f(i)+f(j)+f(i+j)==n-4);  // 注意等号和加号也要算上火柴棒4个
	cout << ans;
	return 0;
}
习题10-5 妖梦拼木棒
#include <cstdio>
#include <algorithm>
#define ll long long
using namespace std;
  
const int kMaxn = 1e6 + 10;
const ll kMod = 1e9 + 7;

ll n, ans, maxa, a[kMaxn], num[kMaxn];
ll C(ll x, ll k) {
	if (k == 1) return x % kMod;
	if (k == 2) return x * (x - 1) / 2 % kMod;
	return 0;
}
 
int main() {
	scanf("%lld", &n);
	for (int i = 1; i <= n; ++i) {
		scanf("%lld", &a[i]);
		maxa = max(maxa, a[i]);
		num[a[i]]++;
	}
	for (int i = 2; i <= maxa; ++i) {
		if (num[i] < 2) continue;
		ll times = C(num[i], 2); // 选择两根相等边
		for (int j = 1; j <= i / 2; ++j) {
			if (j != i - j && num[j] && num[i - j])
				ans = (ans + times * num[j] % kMod * num[i - j] % kMod) % kMod;
			else if (j == i - j && num[j] >= 2)
				ans = (ans + times * C(num[j], 2)) % kMod;
		}
	}
	printf("%lld\n", ans);
	return 0;
}

习题10-6中用到了动态规划(01背包问题板子)和比较深度一些的搜索的知识,我们后期再回来进行学习

习题10-7 Perket
// // 法一:位运算
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
int N,s[12],b[12],mi=2000000001;
int main()
{
	scanf("%d",&N);
	for(int i=0;i<N;i++)
		scanf("%d %d",&s[i],&b[i]);
	for(int i=1;i<(1<<N);i++)//因为至少有一种配料,所以从1开始,有(2^N)-1种情况 对于N种组合,可选与可不选的组合是2^N种,排除0000..000的情况就是2^N 种
	{
		int S=1,B=0;//注意总酸度初始值为1!
		for(int j=0;j<N;j++)
			if((i>>j)&1) // 判断当前组合是否包含第j种配料
			{
				S*=s[j];
				B+=b[j];
			}
		mi=min(mi,abs(S-B));//别忘了绝对值
	}
	printf("%d",mi);
	return 0;
	}
  
// 法二:DFS
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
int N,s[12],b[12],mi=2000000001;
void dfs(int x,int S,int B)
{
	if(x==N)
	{ 
		mi=min(mi,abs(S-B));
		return;
	}
	dfs(x+1,S*s[x],B+b[x]);
	dfs(x+1,S,B);
}

int main()
{
	scanf("%d",&N);
	for(int i=0;i<N;i++)
	scanf("%d %d",&s[i],&b[i]);
	if(N==1)
	{
		printf("%d",abs(s[0]-b[0]));
		return 0;
	}
	dfs(0,1,0);
	printf("%d",mi);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值