暴力美学
一、简单枚举
1.寻找被枚举量之间的关系
- 题意分析:本题看上去是需要枚举两个数字不重复的5位数或4位数(前导0),但是我们发现只要枚举好了第一个数就可以计算出第二个数。然后判断两个数是否数字都不相同。我们可以用一个函数来实现判断两个数是否符合数字不重复这个条件。然后我们可以枚举第一个数字(12345~98765)来确定所有第二个数字。
- 小结:当我们需要枚举两个或更多数字、变量时,我们可以寻找需要枚举的变量之间的关系,通过已经确定的变量来减少需要枚举的步骤达到减少时间复杂度的目的。
2.枚举区间起点与终点
例题博客:紫书 p183 最大乘积(作者:Barsaker)
- 题意分析:如果要求有关连续子序列的问题,在序列长度在时间复杂度允许以内时是可以通过枚举起点和终点来达成目的的。不过在很多题目中,这种方法会因为时间复杂度不够而导致TLE,所以在使用之前需要提前计算时间复杂度。不过在这里需要注意一点,在涉及多个数字相乘的问题时常常需要考虑是否会出现爆int的情况,就像在本题中提到需要使用longlong来储存答案。
- 小结:在求解区间问题时可以枚举起点和终点来实现目的,但是要注意时间复杂度。
3.缩小枚举范围
例题博客:紫书 p183 分数拆分(作者:Barsaker)
- 题意分析:本题如果想不到要缩小范围的话…就写不出来了。这里把书上的分析写一下吧。由于x>=y可得(1/x)<=(1/y),因此(1/k)-(1/y)<=(1/y),即y<=2k。这样我们只需要在2k范围内枚举y然后通过y计算出x即可。
- 小结:暴力是无脑,但又不是完全无脑。在题目中往往是会卡暴力算法的时间复杂度的,但是在有些情况下优化后的暴力说不定是可以过题的。因此在我们思考暴力算法的时候一定要去思考如何去通过数学公式或者其他方法去优化算法。
二、枚举排列
1.生成1~n的排列
用递归的方法来实现字典序排序,n代表数字1~n进行字典序排序
#include<iostream>
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 7;
int n;
bool ch[15];
void dfs( int k,int ans)
{
int res = ans;
if (k == n)
{
cout << res << endl;
return;
}
else
{
for (int i = 1; i <= n; i++)
{
if (!ch[i])
{
res = res * 10 + i;
ch[i] = true;
dfs(k + 1, res);
res /= 10;
ch[i] = false;
}
}
}
}
void solve()
{
while (1)
{
cin >> n;
mem(ch, 0);
dfs(0, 0);
cout << "----------------------------" << endl;
}
}
int main()
{
std::ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
solve();
return 0;
}
输入 4
输出
1234
1243
1324
1342
1423
1432
2134
2143
2314
2341
2413
2431
3124
3142
3214
3241
3412
3421
4123
4132
4213
4231
4312
4321
----------------------------
这里写的代码n不能超过9,如果要优化可以用vector来存答案。
2.生成可重集的排列
如果我们要实现对给定的可以有重复元素的数组进行字典序排列,我们依然可以用递归去实现。
#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;
void print_permutation(int n,int p[],int a[],int cur)//cur指a数组中的元素个数
{
if(cur==n)//当数组a中的元素个数等于数组p中的元素个数时,输出数组a
{
for(int i=0;i<n;i++)cout<<a[i]<<' ';
cout<<'\n';
}
else for(int i=0;i<n;i++)
if(!i||p[i]!=p[i-1])
{
int c1=0,c2=0;
for(int j=0;j<cur;j++)if(a[j]==p[i])c1++;//c1指a数组中片p[i]元素的个数
for(int j=0;j<n;j++)if(p[j]==p[i])c2++;//c2指p数组中p[i]元素的个数
if(c1<c2)
{
a[cur]=p[i];
print_permutation(n,p,a,cur+1);
}
}
}
int main()
{
int n;cin>>n;//n指数组的元素个数
int p[n];int a[n];
memset(a,0,sizeof(a));
for(int i=0;i<n;i++)cin>>p[i];//输入要排列的数组元素;
sort(p,p+n);//将数组按照升序排列
print_permutation(n,p,a,0);
}
3.下一个排列(next_permutation()函数)
<algorithm>头文件提供了一个函数----next_permutation,函数的作用是把当前数组修改为字典序的下一个排列。详细效果见代码分析。
//#pragma GCC optimize(2)
#include<iostream>
#include<iomanip>
#include<cstdio>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
#include<map>
#include<stack>
#include<set>
#include<bitset>
#include<ctime>
#include<cstring>
#include<list>
#define ll long long
#define ull unsigned long long
#define INF 0x3f3f3f3f
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 7;
void solve()
{
int a[3] = { 1,2,3 };
for (int i = 0; i < 3; i++)
cout << a[i] << ' ';
cout << endl;
next_permutation(a, a + 3);
for (int i = 0; i < 3; i++)
cout << a[i] << ' ';
cout << endl;
next_permutation(a, a + 3);
for (int i = 0; i < 3; i++)
cout << a[i] << ' ';
cout << endl;
next_permutation(a, a + 3);
for (int i = 0; i < 3; i++)
cout << a[i] << ' ';
cout << endl;
next_permutation(a, a + 3);
for (int i = 0; i < 3; i++)
cout << a[i] << ' ';
cout << endl;
next_permutation(a, a + 3);
for (int i = 0; i < 3; i++)
cout << a[i] << ' ';
cout << endl;
next_permutation(a, a + 3);
for (int i = 0; i < 3; i++)
cout << a[i] << ' ';
cout << endl;
}
int main()
{
std::ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
solve();
return 0;
}
输出结果为
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
1 2 3
通过代码来看,函数的效果就很明显了。
三、子集生成
1.增量构造法
一个一个枚举…时间复杂度爆炸,想想就头疼。没什么难度,就不多解释了。
2.位向量法
用一个bool数组b[i]来表示第i个数选不选。当选的个数达到要求了就输出。仍然使用递归的方法去实现操作,对第i个数字讨论是否选择后然后继续枚举。
3.二进制法(重点)
从位运算法中我们可以知道这么一件事------对于每一个元素,我们有选择与不选两个选项,而两个选项对应着二进制的0和1。因此,用二进制来表示数字是否被选取是一个不错的主意。
比如对于数组a[3]={1,2,3};
010代表子集{2};
011代表子集{2,3};
我们可以枚举从000到111来表示出所有的子集。
二进制法在实际解题中非常方便!!原因是由于编程语言中已经存在位运算运算符’|’,’&’,’^’。在这里,我们可以用这些操作符号完成对子集的处理。
a[]={1,2,3,4,5};
子集
a1[]={1,2,4};a2[]={2,3};
对应二进制数为a1=11010;a2=01100;
交集:a1&a2=11010&01100=01000={2};
并集:a1|a2=11010|01100=11110={1,2,3,4};
对称差集:a1^ a2=11010^ 01100=10110={1,3,4};
//#pragma GCC optimize(2)
#include<iostream>
#define ll long long
#define ull unsigned long long
#define INF 0x3f3f3f3f
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 7;
void print(int n, int s) //打印0~n-1的子集s
{
for (int i = 0; i < n; i++)
{
if (s & (1 << i))cout << i << ' ';
}
cout << endl;
}
void solve()
{
int n;
cin >> n;
for (int i = 0; i < (1 << n); i++)
print(n, i);
}
int main()
{
std::ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
solve();
return 0;
}
输入 3
输出
0
1
0 1
2
0 2
1 2
0 1 2
一路看下来,二进制法处理子集是最方便的。
四、回溯法(重难点)
在递归枚举的过程中,我们可以把生成、检查过程有机结合起来从而减少不必要的枚举。这就叫回溯法。
例题博客:n皇后问题
回溯:如果当前步骤没有合法答案,则返回上一层递归函数。
仔细理解递归过程中的回溯过程!!!
五、八数码问题(难点进阶)
问题博客:八数码问题
问题分析:九宫格的每一个状态都可以变成最多4种状态,因此我们把每一种状态封装成一个类。对状态就行BFS遍历搜索。
作者:Avalon Demerzel,喜欢我的博客就点个赞吧,更多紫书知识点请见作者专栏《紫书学习笔记》