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)。

改写过程:

\[\begin{align*} f(x) &=a_nx^n+a_{n-1}x^{n-1}+…+a_1x+a_0 \\ &=(a_nx^{n-1}+a_{n-1}x^{n-2}+…+a_1)x+a_0 \\ &=((a_nx^{n-2}+a_{n-1}x^{n-3}+…+a_2)x+a_1)x+a_0 \\ &=… \\ &=((…(a_nx+a_{n-1})x+a_{n-2})x+…)x+a_1)x+a_0\\ \end{align*} \]

秦九韶算法描述:
用朴素算法处理时,计算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;
}

模拟

模拟算法
顾名思义,就是指按照题目的描述去模拟过程,从而获得相应的结果。
模拟算法的特点在于其基本上不需要你去想怎么样去解决,而是本身就给出了解决方案,要你按照解决方案一步一步去实现。

模拟算法的注意事项:

  1. 思考全面,先想好整体的一个框架,再来实现;
  2. 模拟算法更多的是考察代码量,要多练习;
  3. 耐心细致,慢慢调试,多踩坑,多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;
}

课后练习

posted @ 2022-05-09 16:26  HelloHeBin  阅读(237)  评论(0编辑  收藏  举报