🚀 算法入门 | 暴力枚举:从"数方块"到"烤鸡",原来代码可以这么写!
导语 :
提到算法,大家脑海里是不是都是复杂的公式和深奥的数据结构?其实,在算法的世界里,有一种最朴素、最直接的方法,却往往能解决大问题------那就是暴力枚举。
今天,我们通过三道经典的洛谷真题,带你领略一下"暴力枚举"的独特魅力。👇
01 🟦 几何里的"智慧":P2241 统计方形
📝 题目详情
题目背景
1997年普及组第一题
题目描述
有一个 n×mn \times mn×m 方格的棋盘,求其方格包含多少正方形、长方形(不包含正方形)。
输入格式
一行,两个正整数 n,mn, mn,m (n≤5000,m≤5000n \le 5000, m \le 5000n≤5000,m≤5000)。
输出格式
一行,两个正整数,分别表示方格包含多少正方形、长方形(不包含正方形)。
输入输出样例
输入 #1
text
2 3
输出 #1
text
8 10
💡 思路解析
这道题是 NOIP 1997 年的普及组第一题。看到 n,m≤5000n, m \le 5000n,m≤5000 的范围,很多新手会想:"我能不能枚举四个点?" 答案是绝对不行,四重循环会直接超时(TLE)。
这时候的"暴力",需要带一点数学智慧。
- 算总数 :所有的矩形(含正方形)数量 = (横向线段组合数) ×\times× (纵向线段组合数)。即 n(n+1)2×m(m+1)2\frac{n(n+1)}{2} \times \frac{m(m+1)}{2}2n(n+1)×2m(m+1)。
- 算正方形 :我们只需要枚举正方形的边长 ddd。边长为 ddd 的正方形有 (n−d)×(m−d)(n-d) \times (m-d)(n−d)×(m−d) 个。
💻 代码实现
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
long long n,m;
cin>>n>>m;
long long count_z =0;
long long count;
// 计算所有矩形的总数(包含正方形)
count = (n*(n+1)/2)*(m*(m+1)/2);
long long minside = min(n,m);
// 枚举正方形的边长 d
for(long long d=0;d<minside;d++){
count_z+=(n-d)*(m-d);
}
// 输出正方形数量 和 (总数 - 正方形数量)
cout<<count_z<<" "<<count-count_z<<endl;
return 0;
}
02 🍗 循环的艺术:P2089 烤鸡
📝 题目详情
题目背景
猪猪 Hanke 得到了一只鸡。
题目描述
猪猪 Hanke 特别喜欢吃烤鸡(本是同畜牲,相煎何太急!)Hanke 吃鸡很特别,为什么特别呢?因为他有 10 种配料(芥末、孜然等),每种配料可以放 1 到 3 克,任意烤鸡的美味程度为所有配料质量之和。
现在,Hanke 想要知道,如果给你一个美味程度 nnn,请输出这 10 种配料的所有搭配方案。
输入格式
一个正整数 nnn,表示美味程度。
输出格式
第一行,方案总数。
第二行至结束,10 个数,表示每种配料所放的质量,按字典序排列。
如果没有符合要求的方法,就只要在第一行输出一个 0。
输入输出样例
输入 #1
text
11
输出 #1
text
10
1 1 1 1 1 1 1 1 1 2
1 1 1 1 1 1 1 1 2 1
1 1 1 1 1 1 1 2 1 1
1 1 1 1 1 1 2 1 1 1
1 1 1 1 1 2 1 1 1 1
1 1 1 1 2 1 1 1 1 1
1 1 1 2 1 1 1 1 1 1
1 1 2 1 1 1 1 1 1 1
1 2 1 1 1 1 1 1 1 1
2 1 1 1 1 1 1 1 1 1
说明/提示
对于 100% 的数据,n≤10000n \le 10000n≤10000。
💡 思路解析
看到这道题,很多算法高手可能会想:"这不就是整数拆分吗?是不是要用递归?或者生成函数?"
停!打住! 🛑
让我们冷静分析一下数据规模:
- 只有 10 种配料。
- 每种配料只有 3 种选择(1克、2克、3克)。
这意味着总的组合数只有 3103^{10}310 种。
310=590493^{10} = 59049310=59049。
哪怕是最慢的计算机,一秒钟也能跑几亿次运算。6万次运算? 简直是眨眼间的功夫!⚡️
所以,面对这种 "变量少、范围小" 的题目,我们不需要花里胡哨的技巧,只需要祭出我们的终极武器------多重循环嵌套(暴力枚举)!
💻 代码实现
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
cin>>n;
int a0,a1,a2,a3,a4,a5,a6,a7,a8,a9;
int ans=0;
vector<int> v;
// 10层循环,每一层代表一种配料的重量(1到3)
for(int a0=1;a0<=3;a0++){
for(int a1=1;a1<=3;a1++){
for(int a2=1;a2<=3;a2++){
for(int a3=1;a3<=3;a3++){
for(int a4=1;a4<=3;a4++){
for(int a5=1;a5<=3;a5++){
for(int a6=1;a6<=3;a6++){
for(int a7=1;a7<=3;a7++){
for(int a8=1;a8<=3;a8++){
for(int a9=1;a9<=3;a9++){
// 判断总和是否等于美味程度 n
if(a0+a1+a2+a3+a4+a5+a6+a7+a8+a9==n){
ans++;
v.push_back(a0);
v.push_back(a1);
v.push_back(a2);
v.push_back(a3);
v.push_back(a4);
v.push_back(a5);
v.push_back(a6);
v.push_back(a7);
v.push_back(a8);
v.push_back(a9);
}
}
}
}
}
}
}
}
}
}
}
cout<<ans<<endl;
for(int i=0;i<v.size();i++){
cout<<v[i]<<" ";
if((i+1)%10==0){
cout<<endl;
}
}
return 0;
}
03 🔢 集合的妙用:P1618 三连击(升级版)
📝 题目详情
题目描述
将 1,2,...,9 共 9 个数分成三组,分别组成三个三位数,且使这三个三位数的比例是 A:B:CA : B : CA:B:C,试求出所有满足条件的三个三位数,若无解,输出 No!!!。
输入格式
三个数,A,B,CA, B, CA,B,C。
输出格式
若干行,每行 3 个数字。按照每行第一个数字升序排列。
输入输出样例
输入 #1
text
1 2 3
输出 #1
text
192 384 576
219 438 657
273 546 819
327 654 981
说明/提示
保证 0<A<B<C≤9990 < A < B < C \le 9990<A<B<C≤999。
upd 2022.8.3: 新增加二组 Hack 数据。
💡 思路解析
这道题要求我们将 1-9 这九个数字不重复地分配给三个数。
最直接的暴力方法是枚举第一个数 i(范围 123 到 987),然后根据比例算出另外两个数 j 和 k。
关键点在于如何判断数字是否重复?
这里我们使用了一个非常巧妙的工具------set(集合) 。
我们将这三个数的所有位上的数字都扔进 set 里。因为 set 会自动去重,如果最后 set 的大小是 9,且里面没有 0,那就说明 1-9 每个数字刚好用了一次!
💻 代码实现
cpp
#include <iostream>
#include <set>
using namespace std;
int main() {
int a, b, c;
cin >> a >> b >> c;
if(a==0){
cout<<"No!!!"<<endl;
}
else{
set<int> s;
int flag=0;
// 枚举第一个三位数 i
for (int i = 123; i <= 987; i++) {
// 根据比例计算另外两个数
// 注意:这里需要保证整除,否则直接跳过
if ((i * b) % a != 0 || (i * c) % a != 0) continue;
int j = i * b / a;
int k = i * c / a;
// 如果超过三位数范围,直接break
if (j > 999 || k > 999) break;
// 将三个数的每一位放入集合 s 中
s.insert(i / 100); s.insert(i % 100 / 10); s.insert(i % 10);
s.insert(j / 100); s.insert(j % 100 / 10); s.insert(j % 10);
s.insert(k / 100); s.insert(k % 100 / 10); s.insert(k % 10);
// 判断条件:集合大小为9(无重复) 且 集合中没有0
if (s.size() == 9 && s.find(0) == s.end()) {
cout << i << " " << j << " " << k << endl;
flag=1;
}
s.clear(); // 清空集合,准备下一次判断
}
if(flag==0){
cout<<"No!!!"<<endl;
}
}
return 0;
}
📝 总结
通过这三道题,我们可以看到"暴力枚举"并不等于"无脑乱写":
- P2241 告诉我们:当数据量大时,暴力需要结合数学公式来降维打击。
- P2089 告诉我们:当变量少、范围小时,多重循环就是最简单有效的解法。
- P1618 告诉我们:利用 STL容器(如set) 可以极大地简化"判重"和"检查"的过程。
希望这篇推文能帮你更好地理解暴力枚举!如果你觉得有用,记得点赞转发哦!❤️