【前情提要】
此文章包含洛谷题单的枚举题单,共14题,本篇7道题,主要分析思路,并通过这几道题目,进行总结有关枚举的内容。所以内容比较多,可以先收藏起来,慢慢看。
题单链接:暴力枚举https://www.luogu.com.cn/training/108
题目1
分析1
在一个n行m列的棋盘里,找一共有多少正方形和长方形。
而正方形和长方形都属于矩形,区别只有长宽长度的问题,那么可以一并按照矩形分析,当长宽相等时是正方形,否则是长方形。
再接着想,该如何算出棋盘里有多少矩形呢?
首先假设是一个1x2的矩形,把它当作一块积木,放入棋盘中,那么它可以横着放,也可以竖着放。
1. 当它横着放,一共有多少种可能性?(假设棋盘长为n,宽为m)
- 当我们只关注宽时——发现一共有m-1个长度为2的地方,
- 当只关注长时——一共有n个长度为1的地方
最终结果为n*m-1
2. 而竖着放同理。
我们可以大胆猜测,长为a宽为b的矩形一共有(n-a+1)*(m-b+1)
那么我们为了更保险一些,再举个例子,假设求3x3矩形的个数
1. 当它横着放,一共有多少种可能性?(假设棋盘长为n,宽为m)
- 当我们只关注宽时——发现一共有m-2个长度为3的地方,
- 当只关注长时——一共有n-2个长度为3的地方
最终结果为(n-2)*(m-2)
2. 由于是正方形,不需要看竖着的情况
基本正确,为了更严谨一些,可以再尝试举出类似例子。
那根据这个思路,我们可以进一步写出代码,注意范围,那么谈到范围时,我们就要想到假如横着竖着去考虑的话,从横着的1x2到竖着的2x1,假设长方向的设为i,宽方向的设为j
那么对应着i=1,j=2和i=2,j=1,两种情况
而i的范围为从1到n,j的范围为1到m,只需要在里面遍历就可以出现上面这两种情况,所以不需要再多考虑。
代码1
#include<bits/stdc++.h>using namespace std;long long ans,res;int main()
{int n,m;scanf("%d %d",&n,&m);for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){if(i==j)//正方形{ans+=(n-i+1)*(m-i+1);}else{res+=(n-i+1)*(m-j+1);//求长方形,长宽方向已固定,所以不需要看纵向和横向结果 }}}cout << ans << " " << res << endl;return 0;
}
总结1
这题的核心点在于如何求出矩形数量,通常情况下可以多多列举几个例子,去分析,然后推出整体的公式。
题目2
对于 100% 的数据,n≤5000。
分析2
题目意思是给你一个数字m,把10个数字(每个数字范围为1到3)的加和变成这个数字,并输出方案数和具体方案内容。
显而易见10个循环是不行的,超出时间复杂度了。
所以只能另辟蹊径。
用dfs(也就是深度优先遍历)
不懂的可以先看这篇(觉得麻烦也可以不看,下面会简单说明)——dp_背包问题(涉及dfs分析,记忆化搜索)_固定背包体积装物品总价值最大,运筹学问题-CSDN博客
只需要看dfs的部分即可
简单来说,就是先假设第一个数字为1,在这个基础上有三种选择,第二个数字有可能是1,2,3,我们先选择1再进行考虑第三个数字,直到考虑完10个数字.
假设最后结果为m则算是一种方案(然后遍历输入?),回溯到考虑第十个数字,这里我们选择2。
如果结果不为m,则这种方案不算,要去考虑第十个数字,选择2。
理论成立,下面就是代码了。
代码2
#include<iostream>
#include<algorithm>
#include<cstring>using namespace std;const int N=1e5+10;
int a[N][11],s[11];
int n;
int res;void dfs(int x,int p){if(x==10){if(p!=n){return;}for(int i=0;i<10;i++){a[res][i]=s[i];}res+=1;return;}s[x]=1;dfs(x+1,p+1);s[x]=2;dfs(x+1,p+2);s[x]=3;dfs(x+1,p+3);return;
}int main(){scanf("%d",&n);dfs(0,0);//0表示调料的总克数cout<<res<<endl;for(int i=0;i<res;i++){for(int j=0;j<10;j++){printf("%d ",a[i][j]);}printf("\n");}return 0;
}
总结2
这是一道简单的dfs题,按照正常递归思路思考回溯即可
题目3
分析
9个数字,组合成三个数A,B,C,满足一定的比例关系。
那么假如我们已经知道了A,则可以根据比例求出B和C,再去判断满足不满足每个数字只在A,B,C,中出现且仅出现一次的条件。
那么怎么去实现呢?
1. A是怎么遍历的
2. 怎么判断每个数字在A,B,C中有且仅出现一次
首先,A是由1到9这几位数字中抽出三个数字组合而成的。
A的第一位有9种可能性,第二位有除了第一位以外的其余8种可能性,第三位有7种可能性
假设我们嵌套遍历——A的每一位数字,把A弄出来之后,再计算B和C,再设置变量存储B和C的每一位数字,确保每一位都不相等——则输出A,B,C
假如没有输出(可以借用bool变量判断,刚开始定义为false如果输出了则该变量等于true,最后根据这个看是否输出No!!!)则输出No!!!
代码
#include<bits/stdc++.h>using namespace std;int main()
{double a,b,c;bool f=false;cin>>a>>b>>c;if(a==b||a==c||b==c){cout<<"No!!!";return 0;}b/=a;c/=a;for(int i=1;i<10;i++){for(int j=1;j<10;j++){for(int k=1;k<10;k++){if(i!=j&&i!=k&&j!=k){int n1=i*100+j*10+k;int n2=n1*b;int s1=n2%10;int s2=n2%100/10;int s3=n2/100;if(s1!=s2&&s1!=s3&&s2!=s3&&s1!=i&&s1!=j&&s1!=k&&s2!=i&&s2!=j&&s2!=k&&s3!=i&&s3!=j&&s3!=k&&n2<1000&&s1!=0&&s2!=0&&s3!=0){int n3=n1*c;int p1=n3%10;int p2=n3%100/10;int p3=n3/100;if(p1!=p2&&p1!=p3&&p2!=p3&&p1!=i&&p1!=j&&p1!=k&&p1!=s1&&p1!=s2&&p1!=s3&& p2!=i&&p2!=j&&p2!=k&&p2!=s1&&p2!=s2&&p2!=s3&& p3!=i&&p3!=j&&p3!=k&&p3!=s1&&p3!=s2&&p3!=s3&&n3<1000&&p1!=0&&p2!=0&&p3!=0){cout<<n1<<" "<<n2<<" "<<n3<<endl;f=true;}}}}}}if(!f){cout<<"No!!!";}return 0;
}
总结
这题主要是分析,分析出来如何做的写代码就简单了
题目4
分析
题目意思是给你n个数,从中选k个,加和,判断和是否为素数,如果是则种类加1
那么这题代码主要点在于——1. 从n个数中选k个,2 判断和是否为素数
分别考虑如何实现
1. 从n个数中选则k个,首先可以用for循环遍历取得k位数字,也可以用dfs函数,把n个数填在k个格子里面,每个数只能用一次。
2. 判断是否为素数,单开一个函数即可,从素数的定义入手,素数是指从1到本身,它只能%1和它本身时==0,为了简便,可以把范围从sum缩小到sqrt(sum)
理论上我们已经分析出做法了,那么下面就是具体的实施了
代码
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>using namespace std;int a[21];
int k,res,n,flag;
//bool s[21];
int startid;bool issu(int p)
{if (p < 2) return false;for (int i = 2; i <= sqrt(p); i++) {if (p % i == 0) return false;}return true;
}void dfs(int x)
{if(x==k){if(issu(res)){flag++;}return;}else{int start=startid;for(int i=start;i<n;i++){res+=a[i];startid=i+1;dfs(x+1);res-=a[i];startid=start;}}
}
int main()
{cin>>n>>k;for(int i=0;i<n;i++){cin>>a[i];}dfs(0);cout<<flag<<endl;return 0;
}
总结
代码中有个重要的点——》就是在dfs中如何防止元素重复,也就是这个元素用过了,如何防止下次不再用。我们把这部分的代码拿出来分析
首先定义了
int start=startid;
for(int i=start;i<n;i++)
{res+=a[i];startid=i+1;dfs(x+1);res-=a[i];startid=start;
}//把dfs函数参数设置为三个
void dfs(int e,int sum,int x)//x表示下一回的开始
{if(e==k){if(isPrime(sum)){res++;}return;}for(int i=x;i<n;i++){dfs(e+1,sum+s[i],i+1);//下一回从i+1开始}return;
}
这个部分建议记忆,在很多题目中也可能会用到。
题目5
分析
这个题说的是从n个元素中抽出r个元素,输出所有组合,(格式是每个组合数字要占三个字符,用到setw(),或者直接用printf格式化输出)
我们来看如何去写,
依旧是假设有r个位置,要把n个元素不重复的填入进去,每个位置有多种选择,可以用dfs来实现
代码(dfs)
#include<iostream>
#include<algorithm>
#include<iomanip>using namespace std;const int N=21;
int s[N];
int n,r;void dfs(int e,int f,int x)
{if(e==r){for(int i=0;i<r;i++){cout<<setw(3)<<s[i];}cout<<endl;return;}for(int i=f;i<r;i++){for(int j=x;j<=n;j++){s[i]=j;if(s[i-1]<s[i]){dfs(e+1,f+1,j+1);//s[i]=0;//为什么不用还原?因为下一次会被覆盖}}}
}int main()
{scanf("%d%d",&n,&r);dfs(0,0,1);return 0;
}
总结
这个依旧用到了上个问题的不重复写入的方法,但这道题目和上一题不一样的是它需要把每次的结果存入数组中,所以需要再加一个for循环,但是为了不让数组每次遍历都从头开始,再设置一个参数,表示数组的位置。
题目6
分析
这题就是直接给你一共数,输出1到这个数的全排列。
有两种方法
1. 调用库函数next_permutation
2. 自己手写全排列函数
根据这个方法,我们依次分析一下
1. 库函数next_permutation(),里面传入两个参数,第一个是数组的首地址,第二个是数组的末地址,和sort函数差不多,它返回这个数组的下一个全排列
2. 假如自己手写全排列函数,相当于有n个位置,要把n个数填入这几个位置上,每个位置有不止一种选择,用dfs来实现,和上一题差不多,但比那个要简单一些
注意,输出时的元素要占5个位置,用上一题的setw()函数,记得写头文件#include<iomanip>
代码(next_permutation)
#include<iostream>
#include<algorithm>
#include<iomanip>using namespace std;int main()
{int n,l=1;scanf("%d",&n);int s[n];for(int i=1;i<=n;i++){s[i]=i;l*=i;}//全排列一共n!种for(int i=1;i<=l;i++){for(int j=1;j<=n;j++){cout<<setw(5)<<s[j];}cout<<endl;next_permutation(s+1,s+n+1);}return 0;
}
代码(dfs)
#include<bits/stdc++.h>using namespace std;int s[10];
bool a[10];
int n;
int res=1,ans;void dfs(int e)
{if(e>n){for(int i=1;i<=n;i++){cout<<setw(5)<<s[i];}cout<<endl;return;}for(int i=1;i<=n;i++){if(!a[i]){a[i]=true;s[e]=i;dfs(e+1);a[i]=false;}}
}int main()
{scanf("%d",&n);int m=n;while(m!=1){res*=m;m--;}dfs(1);return 0;
}
总结
这里用了另一种防止填入数字重复的方法,只需要定义一个长度和int数组一样大的布尔数组,初始值为false,如果没用过了则可以把它存入int数组并,并更新为true,遍历下一个dfs,同时回溯把刚才的true变为false
题目7
分析
这个题目稍微有些复杂,就是说会给你n个数字,以及要在这个数字组合成的外星文上加的数
主要问题在于如何把这n个数字加m后变成另外n个数字,这个过程是怎样运作的
我们主要观察这个部分:
这是只有三个手指的时候,相当于123的全排列,而且是按照顺序的,所以很自然地想到了用全排列那题的方法,next_permutation()
代码
#include<bits/stdc++.h>using namespace std;int main()
{int n,m;scanf("%d",&n);scanf("%d",&m);int a[n];for(int i=0;i<n;i++){scanf("%d",&a[i]);}for(int i=1;i<=m;i++){next_permutation(a,a+n);}for(int i=0;i<n;i++){printf("%d ",a[i]);}return 0;
}
总结
这题主要是刚开始不太好想到,要善于观察那手指的数字,从中发现规律。