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
#include
#include
using namespace std;
int num[10];
int n;
int main()
{
    scanf("%d",&n);
    for(int i=0;i)
    {
        num[i]=i+1;//赋值1-n 
    }
    do
    {
        for(int j=0;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
#include
#include
#include
#include
#include
using namespace std;
int finger[10010];
int len,shit;//不要在意变量名啊喂!
int main()
{
    cin>>len>>shit;
    for(int i=0;i)
    {
        cin>>finger[i];//输入数组顺序 
    }
    for(int i=0;i)
    {
        next_permutation(finger,finger+len);//进行n次全排列 
    }
    for(int i=0;i)
    {
        cout<" ";//输出全排列后的数组 
    }
    return 0;
}

经过这两个题的历练,大家应该意识到全排列函数的方便之处。

下面我放一段大佬的代码,用递归写全排列函数。(注意与火星人那个题无关了!!!)

#include
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
#include
#include
#include
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//枚举状态
    for(int j=0;j)
    if((1<//判定合法
        int k=a[b[i]-1]%d*num[j+1]%d;//求加上j后的影响
        for(int l=0;l1<//转移
    }
    ans=dp[(1<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;i1]=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
#include
#include
#include
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)
    {
        scanf("%s%lld",dd,&mod);//格式化输入字符数组和mod 
        lenth=strlen(dd);//strlen()函数检测字符数组长度 
        for(int j=0;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)
            {
                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()

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