STL函数库的应用第四弹——全排列(+浅谈骗分策略)
因为基础算法快学完了,图论又太难(我太蒻了),想慢慢学。
所以暂时不写关于算法的博客了,但又因为更新博客的需要,会多写写关于STL的博客。
(毕竟STL函数库还是很香的(手动滑稽))
请出今天主角:STL全排列函数prev_permutation()和next_permutation()
Part 1:引入和导语
首先,我们需要知道,algorithm库里有一些奇怪的函数。
我们在做题的时候,经常会遇到一些全排列的问题(bushi
这时我们通常要用递归搜索解决问题,但是那样会很麻烦,代码也会又臭又长,甚至更加danteng(不会写)
这时怎么办???好消息,万能的STL库又来拯救我们了!
dalao们创造了全排列函数prev_permutation()和next_permutation()
那么怎么用呢?
Part 2:用法和性质
首先,这两个函数都是bool类型的,区别在于:
prev_permutation() 求字典序比当前顺序小的第一个排列,如果有,返回1。如果当前排列已经是字典序最小的排列,返回0。排列结果存在所排列的数组里(会改变数组元素)
next_permutation() 求字典序比当前顺序大的第一个排列,如果有,返回1。如果当前排列已经是字典序最大的排列,返回0。排列结果存在所排列的数组里(会改变数组元素)
使用格式next_permutation(数组名+s,数组名+e)。s表示从第s个元素开始,e表示到第e个元素,对s-e间的元素进行排列。
prev_permutation()格式同上。
这两个函数可以对数字和字符进行全排列操作。
这里需要注意的是:如果你的顺序不是从大到小(或者从小到大)使用函数的时候,并不能生成全排列,只能生成从当前排列的字典序+-1的排列。
所以想要真正的全排列,我们需要先给元素排序(如果全是数字或者字母,用sort()就可以,看情况重载)
如果您不想用sort()可以先用全排列函数排列出字典序最小(或最大)再重新进行全排列。
Part 3:应用与实战
在了解了全排列函数的使用方法和规律后,我们应该拿几个题开刀,熟练一下。
于是我找到了以下几个题:
洛谷P1706 全排列问题
这个题可以说是全排列的板子题了。
题目很简单,输出从1到n的全排列数,只要用上我们的STL函数,代码简直短的不能再短,唯一的问题是,他要求右对齐,五个场宽。
这个可能会让一些萌新挠头,可能会有cout<<"(五个空格)";这种操作。(虽然我有时会做出这种zz操作哈哈哈毕竟我是蒟蒻)
但其实,没有那么复杂,这时格式化输入输出scanf和printf的好处就体现出来了。
在cstdio库里,我们可以这样操作:printf("%5d",a);这样输出,就是右对齐,空出5个场宽。
而且如果输出的数字要占两个场宽,会自动减少空出场宽的量,使输出始终右对齐。
ps:我们还可以printf("%-5d",a);这样是左对齐,空出5个场宽。其他性质与上述一样。
解决了这个问题,此题就变得非常简单,让dalao们康康我的代码:
//P1706 //#include<zhangtao.std> #include<algorithm> #include<cstdio> using namespace std; int num[10]; int n; int main() { scanf("%d",&n); for(int i=0;i<n;i++) { num[i]=i+1;//赋值1-n } do { for(int j=0;j<n;j++) { printf("%5d",num[j]);//输出被全排列的数组 }//全排列的结果会存在数组里(可以认为是调整数组顺序) printf("\n"); } while(next_permutation(num,num+n)!=0)//当返回值为0(无下一个全排列)时,结束循环 return 0; }
洛谷P1088 火星人
先上题目:
题面比较英戳思婷(interesting),你要代表人类和火星人交流?(花里胡哨)
抛开这花里胡哨的题面,我们看本质,题目中给出样例:
123=1
132=2
213=3
231=4
312=5
321=6
很明显,这是1-3的全排列(6种),按字典序分别代表1-6。
题目中给出了排列顺序,给出的顺序代表1。
转化一下问题,其实就是让我们求当前数列按字典序升序的后面第n个排列是什么,所以我们可以用next_permutation()求解。
这样就很简单了,不用sort,我们直接对输入的数组进行n次next_permutation()全排列,然后输出操作后的数组就OK了。
让dalao们康康我的代码。
//P1088 //#include<zhangtao.std> #include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<cmath> using namespace std; int finger[10010]; int len,shit;//不要在意变量名啊喂! int main() { cin>>len>>shit; for(int i=0;i<len;i++) { cin>>finger[i];//输入数组顺序 } for(int i=0;i<shit;i++) { next_permutation(finger,finger+len);//进行n次全排列 } for(int i=0;i<len;i++) { cout<<finger[i]<<" ";//输出全排列后的数组 } return 0; }
经过这两个题的历练,大家应该意识到全排列函数的方便之处。
下面我放一段大佬的代码,用递归写全排列函数。(注意与火星人那个题无关了!!!)
#include<bits/stdc++.h> using namespace std; int n,pd[100],used[100];//pd是判断是否用过这个数 void print()//输出函数 { int i; for(i=1;i<=n;i++) printf("%5d",used[i]);//保留五位常宽 cout<<endl; } void dfs(int k)//深搜函数,当前是第k格 { int i; if(k==n) //填满了的时候 { print();//输出当前解 return; } for(i=1;i<=n;i++)//1-n循环填数 { if(!pd[i])//如果当前数没有用过 { pd[i]=1;//标记一下 used[k+1]=i;//把这个数填入数组 dfs(k+1);//填下一个 pd[i]=0;//回溯 } } } int main() { cin>>n; dfs(0);//注意,这里是从第0格开始的! return 0; }//摘自洛谷大佬Kai_Admin的题解
这样是深度优先搜索+回溯算法的解析,大佬Kai_Admin的代码注解很详细,我这个蒟蒻就不多BB了。
Part 4:全排列函数战术骗分
民间流传着这样一首诗:
骗分过样例,暴力出奇迹。
爆搜挂着机,打表出省一。
这首诗歌颂的是骗分策略。(当然我个人还是建议不要骗分啦)
但是我们有时想不出正解,就不得不用这种令(十)人(分)作(有)呕(效)的手段。
我们常常会遇到一些关于全排列的题目,如果想不出正解,一定不要忘了用这两个全排列函数枚举。
虽然是暴力枚举,但是能拿分,我们永远不嫌分多,对吧?
下面教大家如何有效的骗分。
对于大部分题而言,骗分是一场与时间复杂度之间的较量,是一场与TLE之间的对决。
在确定骗分策略,开始写代码之前,我建议:
1、检查算法复杂度。确定自己的骗分策略可以应付多大的数据,有没有更好的骗分策略(比如打表等),或者你的骗分算法可不可以优化。
2、大概能骗到多少分。如果你对你的骗分算法的时间复杂度很没有把握,那就要看看有没有样例分,要不要直接输出“-1”。(有的题不存在解法会要求输出“-1”)
3、够不够暴力。如果你的骗分策略很复杂,很难写,甚至可能比正解还要冗长,建议把时间留到后面的题,本题直接拿样例分即可。
确定了以上几点,我们就可以进行骗分执法了,下面上一个可以骗分的题目。
可怜的骗分板子题:
洛谷P4163 排列(2007四川OI省选)
给大家看看正解思路吧
没错,就是状态压缩动态规划!
下面是AC代码:
#include<iostream> #include<algorithm> #include<cstdio> #include<cstring> using namespace std; int n,d,S,ans,num[15],f[15],a[15],b[1<<10],c[15],dp[(1<<10)+1][1010];//S为全集,ans为答案,num为数字,f为阶乘,a为10的幂,b为每种状态选了多少个数字,c为计数数组,dp为状压DP数组 inline int read(){//读优 int date=0,w=1;char c=0; while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();} while(c>='0'&&c<='9'){date=date*10+c-'0';c=getchar();} return date*w; } int sum(int x){//求状态x选了多少个数字 int y=0; while(x){ if(x&1)y++; x>>=1; } return y; } void make(){//初始构造阶乘,10的幂,状态的含义 f[0]=a[0]=1;//从0开始。。。 for(int i=1;i<=10;i++){ f[i]=f[i-1]*i; a[i]=a[i-1]*10; } for(int i=0;i<(1<<10);i++)b[i]=sum(i); } void work(){//工作 for(int i=1;i<S;i++)//枚举状态 for(int j=0;j<n;j++) if((1<<j)&i){//判定合法 int k=a[b[i]-1]%d*num[j+1]%d;//求加上j后的影响 for(int l=0;l<d;l++)dp[i][(l+k)%d]+=dp[i^(1<<j)][l];//转移 } ans=dp[(1<<n)-1][0];//记录答案 for(int i=1;i<=n;i++)c[num[i]]++; for(int i=0;i<=9;i++)ans/=f[c[i]];//去重 printf("%d\n",ans);//输出 } void init(){//读入+预处理 char ch[15]; scanf("%s",ch);d=read(); n=strlen(ch); for(int i=0;i<n;i++)num[i+1]=ch[i]-'0';//转化成数字 memset(dp,0,sizeof(dp)); memset(c,0,sizeof(c));//别忘了清空。。。 S=1<<n; dp[0][0]=1;//初始化 } int main(){//主函数So easy! make(); int t=read(); while(t--){ init(); work(); } return 0; }//摘自洛谷大佬斯德哥尔摩的题解
对于大佬的代码,我也不多说,毕竟本蒟蒻根本看不懂,只能%%%。
显然,像我一样的蒟蒻们是不可能会如此高端的算法的。
我们虽然不会正解,但是就真的没法做了吗?
显然不!!!
既然题目中提到了全排列,我们就可以利用全排列函数做点什么。
题目中要求:给出一个仅包含数字0-9的字串和n,要求求出这个字串的全排列中有几个数可以整除n。
我们可不可以直接枚举这个字串的全排列呢?
先检查算法复杂度:
我们先考虑复杂度最坏的情况。
对于0-9这10个数的全排列,一共有10的阶乘共计3628800个,如果加上分离十位数和mod运算的话,复杂度再乘以11。
大概是四千万左右。(9位数仅仅是几百万,8位数几十万,相当于四千万的零头,所以不考虑了。)
题目中给出的时间限制是500ms=0.5s,计算机一秒钟可以运算3-8亿次。
也就是说我们最大最大可以接受的数据范围是:9组10位数的数据。
很不错,这甚至已经接近正解了,毕竟题目中给出的最大数据仅仅有15组。
下面说一下骗分思路:
我们对于每一组输入的数据进行sort排序,使其从小到大排列,然后进行next_permutation()求下一个字典序排列。
再把数组转化成数字(这里要开longlong,因为10位数已经超出了int的表示范围)对给出的数进行模运算,如果运算结果为0,答案++,最后输出答案。
//P4163 //#include<zhangtao.std> #include<algorithm> #include<cstring> #include<cstdio> using namespace std; char dd[10];//定义一个字符数组,用来存输入数据(因为输入数据没有空格) int num[10];//用来把字符数组转化为数字来方便操作 long long x,lenth,mod,n,ans;//不开longlong见祖宗,博文中提到过原因 int main() { scanf("%lld",&n);//输入一共几组数据 for(int i=0;i<n;i++) { scanf("%s%lld",dd,&mod);//格式化输入字符数组和mod lenth=strlen(dd);//strlen()函数检测字符数组长度 for(int j=0;j<lenth;j++) { num[j]=dd[j]-'0';//字符转化数字 } sort(num,num+lenth);//数组从小到大排序,为next_permutation()做准备 //你也可以写cmp函数,从大到小排序。然后用prev_permutation()函数,但是我没那么优秀(闲) do /*do-while语句,如果使用while(next_permutation(num,num+lenth)!=0) 会导致第一种排列方式(从小到大)枚举不到,因为检查条件的时候直接操作了一次*/ { for(int j=0;j<lenth;j++) { x=x*10+num[j];//数组转数字 } if(x%mod==0) ans++;//满足条件,答案++ x=0;//清零变量 } while(next_permutation(num,num+lenth)!=0); //如果还有下一个全排列(函数返回值不为0)进行循环枚举 printf("%lld\n",ans);//输出答案 ans=0;//清空答案,准备下次循环枚举 } return 0;
}
以上是骗分代码,注意它并不是正解!!!
众所周知,洛谷的评测机在不同时间运行速度是不一样的,在人多的时候11个测试点可以AC9个点。
夜深人静的时候,可以AC10个点(第11个测试点495ms读毫秒通过)。
除了第一组数据过于毒瘤,其他全部安全通过,可以说是一套效率很高的骗分策略了。
总而言之,遇到全排列问题的时候,千万不要忘记这两个兄弟:
prev_permutation()
next_permutation()
今天的学习总结就到这里。另外,快开学了,祝大家额,身(少)体(掉)健(头)康(发)吧。