zoukankan      html  css  js  c++  java
  • 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("
    ");
        }
        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
    ",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
    ",ans);//输出答案 
            ans=0;//清空答案,准备下次循环枚举 
        }
        return 0;
    }

    以上是骗分代码,注意它并不是正解!!!

    众所周知,洛谷的评测机在不同时间运行速度是不一样的,在人多的时候11个测试点可以AC9个点。

    夜深人静的时候,可以AC10个点(第11个测试点495ms读毫秒通过)。

    除了第一组数据过于毒瘤,其他全部安全通过,可以说是一套效率很高的骗分策略了。

    总而言之,遇到全排列问题的时候,千万不要忘记这两个兄弟:

    prev_permutation()

    next_permutation()

    今天的学习总结就到这里。另外,快开学了,祝大家额,身(少)体(掉)健(头)康(发)吧。

  • 相关阅读:
    Android listview 的应用
    Android 创建自定义布局
    Android Dialog
    android ProgressBar
    Lilac Pwn stack4-stack_pivoting Writeup
    CTFHUB Pwn ret2shellcode Writeup
    Lilac Pwn stack3-rop Writeup
    函数调用过程与栈帧结构
    线性回归及Python实现
    Google Kick Start Round A 2020
  • 原文地址:https://www.cnblogs.com/zaza-zt/p/12764526.html
Copyright © 2011-2022 走看看