使用火柴棒制作数字的算法

7

我写了一个程序来解决ACM的这个问题

火柴棒是表示数字的理想工具。用火柴棒表示十个十进制数字的常见方法如下:

这与普通闹钟上显示数字的方式完全相同。有了一定数量的火柴棒,您可以生成各种数字。我们想知道使用所有火柴棒可以创建的最小和最大数字是什么。

输入

第一行为正整数:测试用例数,最多100个。之后每个测试用例:

一行包含一个整数n(2≤n≤100):您拥有的火柴棒数量。

输出

每个测试用例:

一行,由一个空格分隔的最小和最大数字。两个数字都应该是正数且不包含前导零。

样例输入

4 3 6 7 15 样例输出

7 7 6 111 8 711 108 7111111

问题在于对于100根火柴棒来说,暴力搜索的搜索树太大了,速度太慢。

以下是前10个数字的结果:

2: 1 1

3: 7 7

4: 4 11

5: 2 71

6: 6 111

7: 8 711

8: 10 1111

9: 18 7111

10: 22 11111

最大值的模式很容易,但我没有看到最小值的快捷方式。有人能提出更好的解决方法吗?这是我使用的代码:

    #include <iostream>
    #include <string>
    using namespace std;

    #define MAX 20 //should be 100

    //match[i] contains number of matches needed to form i
    int match[] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
    string mi[MAX+1], ma[MAX+1];
    char curr[MAX+1] = "";

    //compare numbers saved as strings
    int mycmp(string s1, string s2)
    {
        int n = (int)s1.length();
        int m = (int)s2.length();
        if (n != m)
            return n - m;
        else
            return s1.compare(s2);
    }

    //i is the current digit, used are the number of matchsticks so far
    void fill(int i, int used)
    {
        //check for smaller and bigger values
        if (mycmp(curr, mi[used]) < 0) mi[used] = curr;
        if (mycmp(curr, ma[used]) > 0) ma[used] = curr;

        //recurse further, don't start numbers with a zero
        for (int a = i ? '0' : '1'; a <= '9'; a++) {
            int next = used + match[a-'0'];
            if (next <= MAX) {
                curr[i] = a;
                curr[i+1] = '\0';
                fill(i + 1, next);
            }
        }
    }

    int main()
    {
        //initialise 
        for (int i = 0; i <= MAX; i++) {
            mi[i] = string(MAX, '9');
            ma[i] = "0";
        }

        //precalculate the values
        fill(0, 0);

        int n;
        cin >> n;

        //print those that were asked
        while (n--) {
            int num;
            cin >> num;
            cout << mi[num] << " " << ma[num] << endl;
        }

        return 0;
    }

编辑:最终我使用了动态规划的解决方案。之前我尝试过使用dp,但是我在操作二维状态数组时搞砸了。这里提供的解决方案要好得多。谢谢!


1
这就是递归的问题所在。虽然我不确定它是否适用于你的问题,但你听说过“动态规划”吗?实际上,大多数编程竞赛问题都需要动态规划解决方案。 - Jaywalker
结果可以在线性复杂度下获得。请查看我的答案。 - Benoît
1
这个问题现在已经没有用了,因为外部链接已经失效。这只是说明为什么你不应该在帖子中链接到外部问题而不总结它们。 - Robin Green
6个回答

4
您可以使用动态规划的解决方案。
假设您有n个火柴,您知道如何解决所有n-k个火柴的问题(最小数量),其中k取每个数字使用的火柴数量的值(1使用2个火柴,3使用5个火柴等)。
然后可以递归地得出解决方案。假设您以一个“1”(在最不重要的位置)结束您的数字,则最佳解决方案是通过编写(n-2个火柴的最佳解决方案)1获得的。假设您以一个“2”结束它,则最佳解决方案是(n-5个火柴的最佳解决方案)2。依此类推;最终您可以比较这十个数字,并选择最佳数字。
因此,现在您所要做的就是递归地设计所有小于输入的n的最佳解决方案。
编辑:如果您以直接的方式实现此算法,则最终将得到指数复杂度。这里的技巧是注意到如果您的核心函数MinimumGivenNMatches只需要一个参数n。因此,您将以相同的值调用它很多次。
为了使复杂度线性化,您只需记忆(即记住)每个n的解决方案,使用一个辅助数组。

谢谢!另外需要注意的是,可以通过保持一个额外的数组 predecessor 来找到相应的最佳解决方案;你只需要修改主循环以跟踪这些调用即可。 - Clément
2
只是挑剔一下,记住事情的动态规划方法是记忆化,而不是记忆。不过回答很好。+1 - Bradley Swain

2
为了找到结果:
  • 首先找到最小数字的最小位数
  • 然后从最高有效数字到最低有效数字继续进行。

每个数字都应该被选择,以便存在剩余数字的解决方案。 每个数字需要2到7个匹配。因此,您必须选择留下的数字数量在2 *(N-1)和7 *(N-1)之间的最小第N个“顶部”数字。

不要忘记从搜索结果的最高有效数字中排除0。

顺便说一句,这个算法能够工作的原因之一是对于2到7之间的每个值(匹配),至少存在一个相应的数字。

编辑:10匹配的示例 10匹配-> 2个数字 顶部数字可接受的匹配数= 3到7之间。 需要3到7次匹配的最小数字-> 2(需要5次匹配),排除0。 选定的第一个数字= 2

剩余5次比赛 - > 第二个数字(在这种情况下是最后一个数字)可以接受的匹配数= 精确地5个 需要5次比赛的最小数字 - > 2 选择的第二个数字= 2

结果= 22。

编辑此问题的代码

#include <iostream>
#include <vector>

std::vector<int> nbMatchesForDigit;

long long minNumberForMatches(unsigned int nbMatches)
{
    int nbMaxMatchesForOneDigit = 7;
    int nbMinMatchesForOneDigit = 2;
    int remainingMatches = nbMatches;
    int nbDigits = 1 + nbMatches / nbMaxMatchesForOneDigit; 
    long long result = 0;
    for (int idDigit = 0 ; idDigit < nbDigits ; ++idDigit )
    {
        int minMatchesToUse = std::max(nbMinMatchesForOneDigit, remainingMatches - nbMaxMatchesForOneDigit * (nbDigits - 1 - idDigit));
        int maxMatchesToUse = std::min(nbMaxMatchesForOneDigit, remainingMatches - nbMinMatchesForOneDigit * (nbDigits - 1 - idDigit));
        for (int digit = idDigit > 0 ? 0 : 1 ; digit <= 9 ; ++digit )
        {
            if( nbMatchesForDigit[digit] >= minMatchesToUse && 
                nbMatchesForDigit[digit] <= maxMatchesToUse )
            {
                result = result * 10 + digit;
                remainingMatches -= nbMatchesForDigit[digit];
                break;
            }
        }
    }
    return result;
}

int main()
{
    nbMatchesForDigit.push_back(6);
    nbMatchesForDigit.push_back(2);
    nbMatchesForDigit.push_back(5);
    nbMatchesForDigit.push_back(5);
    nbMatchesForDigit.push_back(4);
    nbMatchesForDigit.push_back(5);
    nbMatchesForDigit.push_back(6);
    nbMatchesForDigit.push_back(3);
    nbMatchesForDigit.push_back(7);
    nbMatchesForDigit.push_back(6);

    for( int i = 2 ; i <= 100 ; ++i )
    {
        std::cout << i << " " << minNumberForMatches(i) << std::endl;
    }
}

1
我认为贪心算法在这种情况下不起作用。相当肯定,这是背包问题的例子。 - Roman
“从最高位到最低位进行”是什么意思?你如何确切地填充它们?我认为这相当不容易。 - tskuzzy
如果我理解正确的话,那么它不起作用;假设你有十个匹配项。你的算法(它是一种贪婪算法)将选择8,然后7,返回78。但是对于十个匹配项,35和22是更好的解决方案。 - Clément
我想我描述我的算法时没有说清楚:请参见我的答案中的示例。 - Benoît

2
使用 动态规划 代替递归。将计算出的值存储起来并重复使用会显著提高速度。实际上,它将指数运行时间转换为多项式时间。
基本思想是使用一个数组 min,它跟踪可以使用恰好 n 根火柴棒制成的最小数字。因此,
min[0] = ""
min[1] = infinity
min[2] = "1"
min[3] = min("1+"min[3-2],"7")
min[4] = min("1"+min[4-2],"7"+min[4-3])
etc

最优解的复杂度是线性的。请看我的回答。 - Benoît
对不起,我不确定如何高效地编写这个算法。 - Benoît
我的解决方案与Clément的相同。基本上只需循环遍历min的值从0N,计算每个元素。然后你的答案就是min[N] - tskuzzy

1
对于最小值,需要注意由于不允许有前导零,因此需要使数字的位数最小化。而最小数字位数为 ceil(n/7)
然后,很容易计算必须在前导数字中使用的火柴棒的最小数量,并从中获取前导数字的最小可能值。

0
#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <map>
using namespace std;

int main()
{
//  freopen("22.txt", "r", stdin);
    vector<char> count(10);
    map<int, char> Min_1;
    Min_1[0] = '8';
    Min_1[2] = '1';
    Min_1[3] = '7';
    Min_1[4] = '4';
    Min_1[5] = '2';
    count[0] = '6';
    count[1] = '2';
    count[2] = '5';
    count[3] = '5';
    count[4] = '4';
    count[5] = '5';
    count[6] = '6';
    count[7] = '3';
    count[8] = '7';
    count[9] = '6';
    int N = 99, P = 2;
    cin >> N;
    while(N --)
    {
        cin >> P;
        vector<char> min, max;
        int a = P, b = P;
        int total = (a + 6) / 7;
        int left = a % 7;
        bool first = true;
        char lastInsert = 0;
        while(a != 0)
        {
            if(total == 1)
            {
                if(left != 6)
                {
                    min.push_back(Min_1[left]);
                }else if(left == 6)
                {
                    if(!first)
                        min.push_back('0');
                    else
                        min.push_back('6');
                }
                break;
            }
            if(left == 0)
            {
                min.insert(min.end(), total, '8');
                break;
            }else{
                if(first && left <= 2)
                {
                    min.push_back('1');
                    lastInsert = 1;
                }else if(first && left < 6)
                {
                    min.push_back('2');
                    lastInsert = 2;
                }else if(first && left == 6)
                {
                    min.push_back('6');
                    lastInsert = 6;
                }else if(!first)
                {
                    min.push_back('0');
                    lastInsert = 0;
                } 
            }
            int temp = count[lastInsert] - '0';
            a -= temp;
            left = a % 7;
            total = (a + 6) / 7;
            first = false;
        }

        if(b % 2 == 1)
        {
            max.push_back('7');
            max.insert(max.end(), (b - 3) / 2, '1');
        }else{
            max.insert(max.end(), b / 2, '1');
        }
        string res_min = string(min.begin(), min.end());
        string res_max = string(max.begin(), max.end());
        cout << res_min << " " << res_max << endl;
        P ++;
    }
    return 0;
}

这是我的回答,希望它有帮助


0

我能够用 O(d) 的时间复杂度解决这个问题,其中 d 是数字的位数。我的想法是,首先我们计算所需最小数字的最小位数。这可以通过 int nofDigits = n/7+ ((0 == n%7) ? 0 : 1) 计算,其中 n 是火柴棒的数量。现在创建一个长度为 nofDigits 的数组。从最低有效位开始,填充最大可能的火柴棒(7),直到最高有效位(MSD)前一位,并在最后将所有剩余的火柴棒分配给最高有效位(MSD)。现在根据 MSD 的火柴棒数量,有三种可能的改进:

第一种情况是,如果 MSD 的火柴棒数量为 1,则我们可以从相邻的数字借用一根火柴棒使其变为 2。这将使相邻数字的火柴棒数量为 6,相当于 0。

第二种情况是,如果 MSD 的火柴棒数量为 4,则与前一种情况相同,我们可以将 MSD 的火柴棒数量增加到 5,相当于 2。

第三步,如果最高位数字的火柴棒数量为3,则我们必须查看数字的总数是否大于2,然后我们可以从最高位数字的两个相邻数字中减去1,否则我们将减少一个相邻数字两次,并将最高位数字的火柴棒数量增加2。

最后,通过遍历数组并用相应的数字替换火柴棒数量来完成程序。

完整的程序:

void minNumWithNMatches_no_dp (int n) {
    if (n < 2) return ;

    int digits[] = {-1, -1, 1, 7, 4, 2, 0, 8};

    int nofDigits = n/7+ ((0 == n%7) ? 0 : 1);

    int *matchesArr = new int [nofDigits];

    int tmp = n;
    for (int i = 0; i < nofDigits; ++i) {
        matchesArr[i] = tmp/7 ? 7 : tmp%7;
        tmp -= matchesArr[i];
    }

    if (nofDigits > 1)
    {
        switch (matchesArr[nofDigits - 1]) {
        case 1:
        case 4:
            {
                --matchesArr[nofDigits - 2];
                ++matchesArr[nofDigits - 1];
                break;
            }
        case 3:
            {
                if (nofDigits > 2) {
                    --matchesArr[nofDigits - 2];
                    --matchesArr[nofDigits - 3];
                } else {
                    matchesArr[nofDigits - 2] -= 2;
                }
                matchesArr[nofDigits - 1] += 2;
                break;
            }
        case 2:
        case 5:
        case 6:
        case 7:
        default:
            {
                ;
            }
        }
    }
    for (int i = nofDigits - 1; i >= 0; --i) {
        int digit = digits[matchesArr[i]];
        if ((i == nofDigits - 1) && (digit == 0)) digit = 6;
        std::cout<< digit;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接