Smallest Difference(最小差值)三种角度求解

前言

最近作者做到了一道题,赶脚这道题十分的beautiful(ugly),老师让我们让三种不同的方法做出来,我苦苦冥想….,最终三种方法都AC了,心里美滋滋~~。How happy I am!(How happy I am!)

题目

题目描述
给定若干位十进制数,你可以通过选择一个非空子集并以某种顺序构建一个数。剩余元素可以用相同规则构建第二个数。除非构造的数恰好为0,否则不能以0打头。
举例来说,给定数字0,1,2,4,6与7,你可以写出10和2467。当然写法多样:210和764,204和176,等等。最后一对数差的绝对值为28,实际上没有其他对拥有更小的差。
Input - 输入
输入第一行的数表示随后测试用例的数量。
对于每组测试用例,有一行至少两个不超过10的十进制数字。(十进制数字为0,1,…,9)每行输入中均无重复的数字。数字为升序给出,相隔恰好一个空格。
Output - 输出
对于每组测试用例,输出一个以上述规则可获得的最小的差的绝对值在一行。
Sample Input - 输入样例
1
0 1 2 4 6 7
Sample Output - 输出样例
28
附上题目链接:Smallest Difference(最小差值)


分析

这道题想必大家也看到了,输入便很恶心,作者一来就将读入优化改了改就可以解决,什么!你不知道读入优化,那先看看作者好基友武ZY的一篇关于读入优化和输出优化的博客吧:读入优化&输出优化
输入处理代码如下:

        int n=0,num[11];
        char c;
        while(1)
        {
            scanf("%c",&c);
            if(c==' ')continue;
            if(c=='\n')break;
            num[++n]=c-'0';
        }

好了,接下来进入正文了,你如何将一串有序而且元素个数还不大于10的数列重新组合拼成两个数并求所有组合出两数的最小差值??说到这里,你肯定想到了爆搜!作者也想到了……但作者没写,貌似不能过。

我们看到这道题有没有想到将这些数进行全排列?然后从中间剪开拼成两个数??没错,这就是正解!我们在读入的时候将这串数长度存为n,同时思考的时候就要想到要让两数差最小就要将他们位数尽量相同,也就是,它们位数尽量接近,不妨让a为较大数,b为较小数,那么就进行分类讨论:当n为偶数时令a位数为n/2,b位数也为n/2,当n为偶数时令a位数为n/2+1,b位数为n/2,如图所示:

Created with Raphaël 2.1.0 判断n n为奇数 a=n/2+1 b=n/2 n为偶数 a=n/2 b=n/2 yes no

好了,接下来就有三种不同的方法:

一、调用全排列函数。

相信很多人第一眼看到这一题就想到了算法函数库(algorithm)中的next_permutation()函数,它的函数作用是将数组中从第i个数至第j个数进行全排列。对于这道题正好,你将它排列出的数进行分割,还要让首位不为 0,枚举所有的数后相减取其中最小值就可以了。这是最好写的一种。代码如下:

#include<deque>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#define INF int(1e9)//1e9=1000000000
using namespace std;
int read()//请忽视(这是读入优化)
{
    int f=1,x=0;
    char c=getchar();
    while(c<'0'||'9'<c){if(c=='-')f=-1;c=getchar();}
    while('0'<=c&&c<='9'){x=x*10+c-'0';c=getchar();}
    return f*x;
}
int num[11];//这是用来存读入的数字的
int main()
{
    char c;
    int t,n=0,ans,tot=0,a,b;
    scanf("%d",&t);
    while(t!=0)
    {
        n=0;
        ans=INF;//注意存答案的变量要赋极大值
        while(1)//这一段就是前面的'伪'读入优化
        {
            scanf("%c",&c);
            if(c==' ')continue;
            if(c=='\n')break;
            num[++n]=c-'0';
        }
        if(n==2)//自己写的next_permutation貌似不能处理长度为二的数列,于是特判
        {
            t--;
            printf("%d\n",num[2]-num[1]);
            continue;
        }
        while(next_permutation(num+1,num+n+1))//进行这些数字的全排列
        {
            if(num[1]!=0&&num[n/2+1]!=0)//判断数字首位不能为0
            {
                a=b=0;
                for(int i=1;i<=n/2;i++)//分割数
                    a=a*10+num[i];  
                for(int j=n/2+1;j<=n;j++)//分割数
                    b=b*10+num[j];
                tot=a-b;
                if(tot<0) tot=-tot;//取绝对值
                if(tot<ans) ans=tot;//给存答案的变量更新
            }
        }
        printf("%d\n",ans);
        t--;
    }
    return 0;
}

二、手写全排列(DFS)。

我们如何手写全排列?如果你用简单的搜索来做肯定要超时,如果你的代码和下面的差不多,那你这题就TLE(超时)了:

#include<deque>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
int n,a[11];
bool flag[11];
void DFS(int x)//枚举当前第几位
{
    if(x>n) //输出方案
    {
        printf("%d",a[1]);
        for(int i=2;i<=n;i++)
            printf(" %d",a[i]);
        printf("\n");
        return; 
    }
    for(int i=1;i<=n;i++)
        if(flag[i]==0)//若这i个数没被用过
        {
            a[x]=i;
            flag[i]=1;//标记
            DFS(x+1);   
            flag[i]=0;//标记清零
        }
    return ;
}
int main()  
{  
    scanf("%d",&n);
    DFS(1);
    return 0;  
}  

所以我们要优化!!!我们可以用一种类似于选择排序的思想,交换第i个数和第j个数,然后进入递归再交换,代码如下:

#include<deque>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#define INF int(1e9)//1e9=1000000000
int read()
{
    int f=1,x=0;
    char c=getchar();
    while(c<'0'||'9'<c){if(c=='-')f=-1;c=getchar();}
    while('0'<=c&&c<='9'){x=x*10+c-'0';c=getchar();}
    return f*x;
}
int num[11],ans,n;
void dfs(int i)
{
    if(i>n)
    {
        if(num[1]!=0&&num[n/2+1]!=0)//仍然判断数字首位不为零
        {
            int a=0,b=0,tot;
            for(int i=1;i<=n/2;i++)
                a=a*10+num[i];  
            for(int j=n/2+1;j<=n;j++)
                b=b*10+num[j];
            tot=a-b;
            if(tot<0) tot=-tot;//取绝对值
            if(tot<ans) ans=tot;
        }
        return ;
    }   
    for(int j=i;j<=n;j++)
    {
        swap(num[i],num[j]);//交换两数再进行判断
        dfs(i+1);
        swap(num[i],num[j]);//交换回来
    }
    return ;
}
int main()
{
    int t=read();
    char c;
    while(t!=0)
    {
        n=0;
        ans=INF;
        while(1)
        {
            scanf("%c",&c);
            if(c==' ')continue;
            if(c=='\n')break;
            num[++n]=c-'0';
        }
        if(n==2)
        {
            t--;
            printf("%d\n",num[2]-num[1]);
            continue;
        }
        dfs(1);
        printf("%d\n",ans);
        t--;
    }
    return 0;
}

三、贪心找最佳方案。

我们还可以通过贪心找最佳方案。

1.如果n为奇数。

这说明之前所设的a必定为较大数,那我们就要让大的数尽量小,小的数尽量大,才能使他们差值最小。于是我们要把最小的一个非零数字给a,如果有零,那作为a的第二位,然后将所剩的数从小到大填入a,直到a的位数达到n/2+1,然后再将剩下的数从大到小给b,那么这样a,b两数差之必然最小。

2.如果n为偶数。

还是设a为较大数,两个位数相等的数他们首位越接近,差就越小于是我们要把所有首位非零的数枚举一遍,找出差值最小的多组数分别作为a,b的首位。还是将较大的数给a,较小的数给b,然后处理方法就和之前的处理方法一样,还是大的数尽量小,小的数尽量大,才能使他们差值最小。如果有零,那作为a的第二位,然后将所剩的数从小到大填入a,直到a的位数达到n/2+1,然后再将剩下的数从大到小给b,那么这样a,b两数差之必然最小(我不会告诉你我写的后面我是复制前面的~~),代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<deque>
#define INF int(1e9)
using namespace std;
int read()
{
    int f=1,x=0;
    char c=getchar();
    while(c<'0'||'9'<c){if(c=='-')f=-1;c=getchar();}
    while('0'<=c&&c<='9'){x=x*10+c-'0';c=getchar();}
    return f*x;
}
int num[11],ans,n,minvalue[11];//minvalue存的有最小差值为mintot的相邻两数
int main()
{
    int t=read(),a,b,mintot,mint;//有最小差值为mintot的相邻两数有mint对
    char c;
    while(t!=0)
    {
        mintot=10;//mintot最多也就等于8对
        mint=a=b=n=0;//a较大数b较小数
        ans=INF;
        while(1)
        {
            scanf("%c",&c);
            if(c==' ')continue;
            if(c=='\n')break;
            num[++n]=c-'0';
        }
        if(n==2)
        {
            t--;
            printf("%d\n",num[2]-num[1]);
            continue;
        }
        else if(n%2!=0)//如果n为奇数,对照前面情况1
        {
            if(num[1]==0)//处理首位为零的情况
            {
                a=num[2]*10;
                for(int i=3;i<=(n+1)/2;i++)
                    a=a*10+num[i];
            }
            else//首位不为零
            {
                for(int i=1;i<=(n+1)/2;i++)
                    a=a*10+num[i];
            }
            for(int i=n;i>=(n+1)/2+1;i--)
                b=b*10+num[i];
            ans=a-b;
        }
        else //如果n为偶数,对照前面情况2
        {
            for(int i=2;i<=n;i++)//找最小差值为mintot的相邻两数有几对
            {
                if(num[i]-num[i-1]<mintot&&num[i-1]!=0)//一定要首位不为零
                {
                    mintot=num[i]-num[i-1];
                    mint=1;
                    minvalue[mint]=i;
                }
                else if(num[i]-num[i-1]==mintot&&num[i-1]!=0)
                    minvalue[++mint]=i;
            }
            for(int i=1;i<=mint;i++)//分别尝试这最小差值为mintot的相邻两数
            {
                a=num[minvalue[i]]; 
                b=num[minvalue[i]-1];
                int j=1,t=0;
                while(t<n/2-1)
                {
                    if(j!=minvalue[i]&&j!=minvalue[i]-1)
                    {
                        a=a*10+num[j];
                        t++;    
                    }
                    j++;
                }
                j=n,t=0;
                while(t<n/2-1)
                {
                    if(j!=minvalue[i]&&j!=minvalue[i]-1)
                    {
                        b=b*10+num[j];
                        t++;    
                    }
                    j--;
                }
                if(a-b<ans)
                    ans=a-b;
            }
        }
        printf("%d\n",ans);
        t--;
    }
    return 0;
}

总结

这三种方法各有各的好处,但我们看看三种方法性能的对比(手打的我知道很丑..):
| 方法——当题状况————耗时——内存—代码长度 |
| 一方法一 |
|———————————————————————|
| 二 方法二|
|———————————————————————|
| 三 方法三 |
|———————————————————————|
所以这道题贪心是最好的!!!

猜你喜欢

转载自blog.csdn.net/qq_37555704/article/details/78613681