06. 模拟&枚举
何谓算法
算法是结题方案的准确而完整的描述,是一系列解决问题的清晰指令,
算法代表着用系统的方法描述解决问题的策略机制。
也就是说,算法就是解决问题的方法,能够对一定规范的输入,在有限时间内获得所要求的输出。
常见的算法有递推法,递归法,穷举法,贪心算法,分治法,动态规划,回溯法等。
算法的5个特征:
(1)有穷性: 一个算法必须保证执行有限步之后结束;
(2)确切性: 算法的每一步骤必须有确切的定义,无二义性;
(3)输入:一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定除了初始条件;
(4)输出:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
(5)可行性:可通过基本运算有限次执行来实现,也就是算法中每一个动作能够被机械地执行。
秦九韶算法是南宋时期秦九韶提出的一种多项式简化算法:
多项式 \(f(x)=a_nx^n+a_{n-1}x^{n-1}+…+a_1x+a_0\) 在代入一个特定的x进行f(x)整体求值时,用朴素算法需要经过 n(n+1)/2 次乘法操作和n次加法操作。
而使用秦九韶算法通过去除冗余步骤,只需要进行n次乘法和n次加法操作就可以计算出f(x)。
改写过程:
秦九韶算法描述:
用朴素算法处理时,计算X的次幂过程中会造成很多无效操作。而秦九韶算法层层嵌套,避开了不必要的计算。
该算法描述如下:
第一步:输入f(x)的系数an, an-1,… a1, a0 ,输入x的值。
第二步:答案ans初始化为0。
第三步:对于 i=n, n-1, n-2,…,1,0,循环执行 ans=ans*x+ ai;
第四步:输出ans
枚举
枚举,也叫穷举法。即指列出所有情况,并逐一分析。
算法的特点是它的正确性一般比较明显,十分直观,编写上大多情况下也较为容易。
缺点在于,由于需要枚举所有情况,当情况很多时效率一般很低,甚至可能达到指数级。
大多数的搜索算法本质上都是枚举算法,只是其中加入了不同的优化,从而提升了时间效率,使得正确性和效率之间达到了更好的平衡。
【例】质数
定义质数为因数只有1和其本身的数,对于 n 组询问,试判断每个数是否为质数。
输入格式:读入第一行一个正整数n,表示有n组询问;
接下来n行,每行一个正整数m,表示询问m是否为质数,是则输出“yes”,否则输出“no”
输出格式:n行,每行一个字符串,代表答案。
数据范围:\(n<=10^3, 1<m<=10^10\)
【分析】从定义,考虑枚举每一个1到m的数,依次判断它是不是m的因数,
若统计得出m一共只存在2个因数,则能判断m是一个质数;否则m是一个合数。
面对10^6范围内的m,使用穷举法是可行的,
但随着m范围的增大,枚举所有的数就显得十分冗余,并且过大的时间复杂度是无法接受的。
观察后发现一个数的因数都是成对出现的,完全平方数的平方根除外,
如 30=1*30=2*15=3*10=5*6,1和30是一对因子,2和15是一对因子…。
且每一对因子中一定有一个是小于等于平方根的(设ab=m,a<=b,则有a^2<=ab=m, 因此a<=平方根)。
只要出现一个非1非本身的约数,就可以判定该数不是质数。
于是只需从2到平方根进行枚举、看是否有m的约数就可以了。
【注意】i*i <= n, 那么对 i 的大小也具有要求,不能过大。
- 参考程序
int main(){
int n,m; cin>>n;
for(int i=1; i<=n; i++){
cin>>m;
bool isprime=true; //假设 m是质数
for(int j=2; j*j<=m; j++){
if(m%j==0) { //发现 m存在其它因数,则 m是合数
isprime=false; break;
}
}
printf("%s\n", isprime || m==1 ? "yes" : "no");
}
return 0;
}
【例】三连击
将1,2,...,9共9个数分成3组,分别组成3个三位数,且使这3个三位数构成1:2:3的比例,
试求出所有满足条件的3个三位数。
输入格式:无输入
输出格式:若干行,每行3个数字。按照每行第1个数字升序排列。
输入样例:无
输出样例:
192 384 576
.......
- 参考程序
#include<iostream>
using namespace std;
int main(){
for(int i=100; i<=333; i++){
int a=i, b=2*i, c=3*i, vis[10]={0}, sum=0;
vis[a/100] = vis[a/10%10] = vis[a%10] = 1;
vis[b/100] = vis[b/10%10] = vis[b%10] = 1;
vis[c/100] = vis[c/10%10] = vis[c%10] = 1;
for(int j=1; j<=9; j++) sum += vis[j];
if(sum==9) cout<<a<<" "<<b<<" "<<c<<endl;
}
return 0;
}
【例】三连击(升级版)
将 1,2,…,9 共 9 个数分成三组,分别组成三个三位数,且使这三个三位数的比例是 A:B:C,
试求出所有满足条件的三个三位数,若无解,输出 No!!!。保证 A<B<C。
输入格式:三个数,A,B,C。
输出格式:若干行,每行 3 个数字。按照每行第一个数字升序排列。
输入样例:1 2 3
输出样例:
192 384 576
219 438 657
273 546 819
327 654 981
- 参考程序
#include<iostream>
using namespace std;
int main(){
int A,B,C,cnt=0; cin>>A>>B>>C;
for(int i=100; i<=333; i++){
int a=i, b=B*i/A, c=C*i/A, vis[10]={0}, sum=0;
vis[a/100] = vis[a/10%10] = vis[a%10] = 1;
vis[b/100] = vis[b/10%10] = vis[b%10] = 1;
vis[c/100] = vis[c/10%10] = vis[c%10] = 1;
for(int j=1; j<=9; j++) sum += vis[j];
if(sum==9){ printf("%d %d %d\n",a,b,c); cnt++; }
}
if(cnt==0) printf("No!!!\n"); return 0;
}
【例】垃圾炸弹
2014年足球世界杯开踢啦!
为方便球迷观看比赛,街道上很多路口都放置了直播大屏幕,但是人群散去后总会在这些路口留下一堆垃圾。
为此政府决定动用一种最新发明-“垃圾炸弹”。
这种“炸弹”利用最先进的量子物理技术,爆炸后产生的冲击波可以完全清除波范围内的所有垃圾,并且不会产生任何其它不良影响。
炸弹爆炸后冲击波是以正方形方式扩散的,爆炸威力以d给出,表示可以传播d条街道。
假设城市的布局为严格的[0,1024]*[0,1024]的网格状,由于财政问题,市政府只买得起一枚“垃圾炸弹”,
希望你帮他们找到合适的投放地点,使得一次清除的垃圾总量最多
(垃圾数量可以用一个非负整数表示,除大屏幕的路口以外没有垃圾)。
输入格式:
第一行给出“炸弹”威力d;
第二行给出一个数组n,表示设置了大屏幕(有垃圾)的路口数量;
接下来n行,每行给出三个数字x,y,i,分别代表路口的坐标(x,y)以及垃圾数量i。
点坐标(x,y)保证是有效的(区间0到1024之间),同一坐标只会给出一次。
输出格式:输出能清理垃圾最多的投放点数目,以及能够清除的垃圾总量。
数据范围:d<=50,n<=1000
输入样例:
1
2
4 4 10
6 6 20
输出样例:
1 30
【分析】
假设n<=50,那么我们可以直接枚举出最终的投放点(x,y),
然后枚举n个路口判断这个投放点是否能覆盖到这些路口,最终输出答案即可。
但算法的运行时间为 \(1024^2*n\),当n的规模到了1000后会超时。
题目中保证n<=50,那么是否能从这里入手?
维护数组cnt[i][j]表示炸弹投放点在(i,j)时的答案,枚举每个垃圾(x,y),
那么它能对[x-d,x+d]*[y-d,y+d]的投放点造成加1的影响,
枚举[x-d,x+d]*[y-d,y+d]内的所有点并对相应的cnt加1,最终扫一遍cnt就能得到答案了。
该算法的复杂度为 \(O(n*d^2)\),能1s内完美解决这道题。
- 参考程序
#include<iostream>
using namespace std;
const int N=1e3+30;
int cnt[N][N],d,n,x,y,num, max_v=-1, max_cnt=1;
int main(){
cin>>d>>n;
for(int i=1; i<=n; i++){
cin>>x>>y>>num;
for(int xr=-d; xr<=d; xr++){
for(int yr=-d; yr<=d; yr++){
if(x+xr>=0&&x+xr<1025&&y+yr>=0&&y+yr<1025){
cnt[x+xr][y+yr] += num;
}
}
}
}
for(int i=0; i<1025; i++){
for(int j=0; j<1025; j++){
if(max_v<cnt[i][j]) max_v=cnt[i][j], max_cnt=1;
else if(max_v==cnt[i][j]) max_cnt++;
}
}
cout<<max_cnt<<" "<<max_v<<endl;
return 0;
}
模拟
模拟算法
顾名思义,就是指按照题目的描述去模拟过程,从而获得相应的结果。
模拟算法的特点在于其基本上不需要你去想怎么样去解决,而是本身就给出了解决方案,要你按照解决方案一步一步去实现。
模拟算法的注意事项:
- 思考全面,先想好整体的一个框架,再来实现;
- 模拟算法更多的是考察代码量,要多练习;
- 耐心细致,慢慢调试,多踩坑,多debug。
【例】众数
对于一个长度为n的序列 {an} 来说,其众数被定义为出现次数最多的数。
现在给定一个长度为n的序列,yc想要你求出他的众数是多少。
当然众数可能有很多个,你只需要输出最小的一个就可以了。
数据范围:N<1e6,0<ai<1000
输入格式:第一行输入n,第二行输入n个数。
输出格式:输出众数。
输入样例:
6
3 5 7 5 3 1
输出样例:3
- 参考程序
#include<iostream>
using namespace std;
const int N=1000005,M=1005;
int n,a[N],s[M],cnt=0;
int main() {
cin>>n;
for(int i=1; i<=n; i++) cin>>a[i];
for(int i=1; i<=n; i++) ++s[a[i]];
for(int i=1; i<M; i++){
if(s[i]>s[ret]) ret=i;
}
cout<<ret<<endl; return 0;
}
【例】幻方
幻方是一种很神奇的 N*N矩阵:它由数字1,2,…,N*N组成。且每行每列以及两条对角线上的数字和都相同。
当N为奇数时,我们可以通过以下方式构建一个幻方:
(1)将1写在一行的中间。
(2)按如下方式从小到大一次填写每个数K(K=2,3,…,N*N):
若K-1在第一行但不在最后一列,则将K填在最后一行,K-1所在列的右一列。
若K-1在最后一列但不在第一行,则将K填在第一列,K-1所在行的上一行。
若K-1在第一行最后一列,则将K填在K-1的下方。
若K-1既不在第一行,也不在最后一列,如果K-1的右上方还未填数,则将K填在K-1的右上方,否则填在K-1的正下方。
现给定N请按上述方法构造 N*N的幻方。
1<=N<=39且 N为奇数。
输入样例:3
输出样例:
8 1 6
3 5 7
4 9 2
- 参考程序
#include<bits/stdc++.h>
using namespace std;
const int N=40;
int a[N][N];
int main() {
int n; cin>>n;
int x=1, y=n/2+1; a[x][y]=1;
for(int i=2; i<=n*n; i++){
if(x==1 && y!=n){
a[n][y+1]=i;
x=n, y=y+1;
}else if(y==n && x!=1){
a[x-1][1]=i;
x=x-1, y=1;
}else if(x==1 && y==n){
a[x+1][y]=i;
x=x+1;
}else if(x!=1 && y!=n){
if(a[x-1][y+1]==0){
a[x-1][y+1]=i;
x=x-1, y=y+1;
}else{
a[x+1][y]=i;
x=x+1;
}
}
}
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
cout<<setw(5)<<a[i][j];
}cout<<endl;
}
return 0;
}