只由数字0和1组成的给定数的最小倍数

5
给定一个整数 N,你需要找到由数字 0 和 1 组成的 N 的最小倍数。由于这个倍数可能很大,因此请以字符串形式返回。

返回的字符串不应包含前导零。

例如,对于 N = 55,110 是由数字 0 和 1 组成的最小倍数。 对于 N = 2,答案为 10。

我看到了几个相关的问题,但是我没有在我的代码中找到问题所在,即使使用 map 而不是 set,它仍然在某些情况下超时。

#define ll long long
int getMod(string s, int A)
{
    int res=0;
    for(int i=0;i<s.length();i++)
    {
        res=res*10+(s[i]-'0');
        res%=A;
    }
    return res;
}
string Solution::multiple(int A) {
    if(A<=1)
    return to_string(A);

    queue<string>q;
    q.push("1");
    set<int>st;
    string s="1";

    while(!q.empty())
    {
        s=q.front();
        q.pop();
        int mod=getMod(s,A);
        if(mod==0)
        {
            return s;
        }
        else if(st.find(mod)==st.end())
        {
            st.insert(mod);
            q.push(s+"0");
            q.push(s+"1");
        }
    }

}

正如这些类型的谜题和测验网站一样,它们在教授C++方面没有任何实际用途。这只是一个数学技巧,你必须知道正确的数学公式。这与C++无关,而与数学有关。如果你的目的是学习和提高你的C++技能,你会发现花时间阅读一本好的C++书籍比浪费时间在这些谜题上要更有效。你不应该再浪费时间在这上面了,而是回到你的C++书籍。 - Sam Varshavchik
1
“map”和“set”的时间复杂度几乎相同,并且您正在进行大量的内存分配。我认为您需要一个更好的算法。 - molbdnilo
1
@SamVarshavchik先生,感谢您宝贵的演讲,但我解决这些问题并不是为了学习C++。我只是使用C++来解决这些数学/谜题技巧。 - Saurabh Verma
1
@SaurabhVerma N 的最大值是多少? - nice_dev
4
这个网址https://math.stackexchange.com/questions/388165/how-to-find-the-smallest-number-with-just-0-and-1-which-is-divided-by-a-give回答了你的问题吗? - User_67128
显示剩余4条评论
3个回答

7

这里是Raku语言的实现。

my $n = 55;
(1 .. Inf).map( *.base(2) ).first( * %% $n );

(1 .. Inf)是一个从一到无穷的懒惰列表。 "星号" * 建立了一个闭包,代表map中的当前元素。

base是Rakus Num类型的一个方法,它返回所需基数下给定数字的字符串表示,这里是一个二进制字符串。

first返回当“星号”闭包对其为真时的当前元素。

%%是“可被整除”运算符,它隐式地将其左侧转换为Int

哦,并且最重要的是,这很容易进行并行化,因此您的代码可以使用多个cpu核心:

 (1 .. Inf).race( :batch(1000), :degree(4) ).map( *.base(2) ).first( * %% $n );

1
n的限制是多少?它对于n = 60000007会产生输出吗?如果会,那需要多长时间? - User_67128
1
关于限制,Raku支持任意长度的整数,因此我认为没有固定的限制。 - Holli
与我的Java实现相比,它要慢得多。 - User_67128
好的,我在一台2010年的i4处理器上以3点几GHz的速度运行了它。然而,Raku并不是最快的编程语言。至少目前还不是^^ - Holli
你的实现非常出色,代码很短。然而,你基本上使用了与 OP 相同的算法。这就是为什么你的程序在处理大量输入值时变得非常缓慢的原因。例如,当 N = 99999999 时,结果由 72 个连续的“1”组成... - Damien
显示剩余4条评论

2
如“数学”参考中所述,结果与模A下10的幂的同余有关。
如果
n = sum_i a[i] 10^i

那么

n modulo A = sum_i a[i] b[i]

如果a[i]等于0或1,且b[i] = (10^i)模A

那么问题就是找到最小的a[i]序列,使得它们的和模A等于0。

从图的角度来看,我们需要找到到0模A的最短路径。

使用BFS通常很适合找到这样的路径。问题在于可能会指数级增加要访问的节点数量。在这里,通过拒绝已经获得(在程序中看到的向量used)的模A的和的节点,我们可以确保最终得到的节点数量小于A。请注意,为了获得最小的数字,这种拒绝是必需的。

下面是一个C++程序。由于解决方案相当简单,即使对C++不熟悉的人也应该很容易理解。

#include <iostream>
#include <string>
#include <vector>

struct node {
    int sum = 0;
    std::string s;
};

std::string multiple (int A) {
    std::vector<std::vector<node>> nodes (2);
    std::vector<bool> used (A, false);
    int range = 0;
    int ten = 10 % A;
    int pow_ten = 1;

    if (A == 0) return "0";
    if (A == 1) return "1";

    nodes[range].push_back (node{0, "0"});
    nodes[range].push_back (node{1, "1"});
    used[1] = true;

    while (1) {
        int range_new = (range + 1) % 2;
        nodes[range_new].resize(0);
        pow_ten = (pow_ten * ten) % A;

        for (node &x: nodes[range]) {
            node y = x;
            y.s = "0" + y.s;
            nodes[range_new].push_back(y);
            y = x;
            y.sum = (y.sum + pow_ten) % A;
            if (used[y.sum]) continue;
            used[y.sum] = true;
            y.s = "1" + y.s;
            if (y.sum == 0) return y.s;
            nodes[range_new].push_back(y);
        }
        range = range_new;
    }
}

int main() {
    std::cout << "input number: ";
    int n;
    std::cin >> n;
    std::cout << "Result = " << multiple(n) << "\n";
    return 0;
}

编辑

上述程序使用了一种记忆化的方法来加速处理过程,但对于大型输入,内存变得太大。例如,在评论中指出,它无法处理N = 60000007的情况。

我通过以下修改改进了速度和范围:

  • 创建了一个函数(reduction),用于简化当输入数字可被2或5整除时的搜索
  • 为了记忆节点(nodes数组),现在只使用一个数组而不是两个
  • 使用了一种类似于meet-in-the-middle的过程:首先,函数mem_gen记忆了所有相关的01序列,最多有N_DIGIT_MEM(=20)个数字。然后主过程multiple2生成有效的01序列“在前20个数字之后”,然后在记忆中寻找“补充序列”,使得两者的连接是一个有效序列

使用这个新程序,当N = 60000007时,在我的电脑上大约需要600毫秒才能提供正确结果(100101000001001010011110111,27位数)。

编辑2

与其限制第一步记忆化的数字数量,我现在使用内存大小作为阈值,因为这个大小不仅取决于数字数量,还取决于输入数字。请注意,这个阈值的最佳值将取决于输入数字。在这里,我选择了50k作为折衷方案的阈值。对于20k的阈值,对于60000007,我在36毫秒内获得了正确结果。此外,对于100k的阈值,最坏情况99999999可以在5秒内解决。

我进行了不到10^9的值的不同测试。在几乎所有测试的情况下,结果都在1秒内提供。然而,我遇到了一个角落案例N = 99999999,其中结果由72个连续的“1”组成。在这种特殊情况下,程序需要大约6.7秒。对于60000007,正确结果在69毫秒内获得。

以下是新程序:

    #include <iostream>
    #include <string>
    #include <vector>
    #include <map>
    #include <unordered_map>
    #include <chrono>
    #include <cmath>
    #include <algorithm>

    std::string reverse (std::string s) {
        std::string res {s.rbegin(), s.rend()};
        return res;
    }

    struct node {
        int sum = 0;
        std::string s;
        node (int sum_ = 0, std::string s_ = ""): sum(sum_), s(s_) {};
    };

//  This function simplifies the search when the input number is divisible by 2 or 5
    node reduction (int &X, long long &pow_ten) {
        node init {0, ""};
        while (1) {
            int digit = X % 10;
            if (digit == 1 || digit == 3 || digit == 7 || digit == 9) break;
            switch (digit) {
                case(0):
                    X /= 10;
                    break;
                case(2):
                case(4):
                case(6):
                case(8):
                    X = (5*X)/10;
                    break;
                case(5):
                    X = (2*X)/10;
                    break;
            }
            init.s.push_back('0');
            pow_ten = (pow_ten * 10) % X;
        }       
        return init;
    }

const int N_DIGIT_MEM = 30;     // 20
const int threshold_size_mem = 50000;

//  This function memorizes all relevant 01 sequences up to N_DIGIT_MEM digits
bool gene_mem (int X, long long &pow_ten, int index_max, std::map<int, std::string> &mem, node &result) {

    std::vector<node> nodes;
    std::vector<bool> used (X, false);
    bool start = true;

    for (int index = 0; index < index_max; ++index){
        if (start) {
                node x = {int(pow_ten), "1"};
                nodes.push_back (x);
        } else {
            for (node &x: nodes) {
                x.s.push_back('0');
            }
            int n = nodes.size();

            for (int i = 0; i < n; ++i) {
                node y = nodes[i];
                y.sum = (y.sum + pow_ten) % X;
                y.s.back() = '1';
                if (used[y.sum]) continue;
                used[y.sum] = true;
                if (y.sum == 0) {
                    result = y;
                    return true;
                }
                nodes.push_back(y);
            }   
        }
        pow_ten = (10 * pow_ten) % X;
        start = false;
        int n_mem = nodes.size();
        if (n_mem > threshold_size_mem) {
            break;
        }
    }
    for (auto &x: nodes) {
        mem[x.sum] = x.s;
    }
    //std::cout << "size mem = " << mem.size() << "\n";
    return false;
}
//  This function generates valid 01 sequences "after the 20 first digits" and then in the memory 
//  looks for a "complementary sequence" such that the concatenation of both is a valid sequence
    std::string multiple2 (int A) {
        std::vector<node> nodes;
        std::map<int, std::string> mem;
        int ten = 10 % A;
        long long pow_ten = 1;
        int digit;

        if (A == 0) return "0";
        int X = A;
        node init = reduction (X, pow_ten);

        if (X != A) ten = ten % X;

        if (X == 1) {
            init.s.push_back('1');
            return reverse(init.s);
        }
        std::vector<bool> used (X, false);
        node result;
        int index_max = N_DIGIT_MEM;
        if (gene_mem (X, pow_ten, index_max, mem, result)) {
            return reverse(init.s + result.s);
        }   

        node init2 {0, ""};
        nodes.push_back(init2);

        while (1) {
            for (node &x: nodes) {
                x.s.push_back('0');
            }
            int n = nodes.size();
            for (int i = 0; i < n; ++i) {
                node y = nodes[i];
                y.sum = (y.sum + pow_ten) % X;
                if (used[y.sum]) continue;
                used[y.sum] = true;
                y.s.back() = '1';
                if (y.sum != 0) {
                    int target = X - y.sum;
                    auto search = mem.find(target);
                    if (search != mem.end()) {
                        //std::cout << "mem size 2nd step = " << nodes.size() << "\n";
                        return reverse(init.s + search->second + y.s);
                    }
                }
                nodes.push_back(y);
            }
            pow_ten = (pow_ten * ten) % X;
        }
    }

    int main() {
        std::cout << "input number: ";
        int n;
        std::cin >> n;
        std::string res;

        auto t1 = std::chrono::high_resolution_clock::now();
        res = multiple2(n),
        std::cout << "Result = " << res << "  ndigit = " << res.size() << std::endl;
        auto t2 = std::chrono::high_resolution_clock::now();
        auto duration2 = std::chrono::duration_cast<std::chrono::microseconds>( t2 - t1 ).count();
        std::cout << "time = " << duration2/1000 << " ms" << std::endl;

        return 0;
    }

1
对于输入1,这将返回10。 - גלעד ברקן
1
@User_67128 最初,OP没有提到最大尺寸,然后他们提到了10^9的限制...我的程序使用一种记忆化方法来加速处理过程,但对于大输入,内存变得太大了...我花时间尝试改进算法。我只通过使用一种中间相遇的方法获得了相当小的收益。我没有发布这个新解决方案,因为我无法处理非常大的情况。然而,使用我的最新程序,对于60000007,我在约1秒钟内得到了解决方案100101000001001010011110111(27位数字)。如果您认为仍然有趣,我可以发布它。 - Damien
1
@User_67128,使用程序的最新版本,60000007案例已在69ms内得到解决。 - Damien
1
@User_67128 对于600000007,大约需要解决140毫秒。答案是1100101001010100101001000010001(31位数)。 - Damien
1
我正在考虑用Java实现您的想法,我已经有一个基于树结构的Java实现,但速度要慢得多。 - User_67128
显示剩余6条评论

0

对于更熟悉Python的人,这里是@Damien代码的转换版本。Damien的重要见解是强烈减少搜索树,利用每个部分和只需要被调查一次的事实,即第一次遇到它时。

该问题也在Mathpuzzle中描述,但他们主要关注解决方案的必要存在性。在整数序列在线百科全书中也提到了代码。sage版本似乎有些相似。

我做了一些改变:

  • 从空列表开始有助于正确解决A=1,同时简化代码。将乘以10的操作移到循环末尾。对于0做同样的操作似乎很困难,因为log10(0)负无穷大
  • 不再在nodes[range]nodes[new_range]之间交替,而是使用两个不同的列表。
  • 由于Python支持任意精度的整数,部分结果可以存储为十进制或二进制数字,而不是字符串。以下代码尚未实现此功能。
from collections import namedtuple

node = namedtuple('node', 'sum str')

def find_multiple_ones_zeros(A):
    nodes = [node(0, "")]
    used = set()
    pow_ten = 1
    while True:
        new_nodes = []
        for x in nodes:
            y = node(x.sum, "0" + x.str)
            new_nodes.append(y)
            next_sum = (x.sum + pow_ten) % A
            y = node((x.sum + pow_ten) % A, x.str)
            if next_sum in used:
                continue
            used.add(next_sum)
            y = node(next_sum, "1" + x.str)
            if next_sum == 0:
                return y.str
            new_nodes.append(y)
        pow_ten = (pow_ten * 10) % A
        nodes = new_nodes

A = 1000002 时,开始显著减慢。原帖中提到输入可能高达10^9。 - גלעד ברקן
显然,10^9不是一个严肃的可能性。使用32位有符号整数的C++代码将在A> 214,748,365时停止工作。但是,即使对于更低的数字,谜题也存在太多可能性需要测试,以期望在合理的时间内得到解决方案。Python和C++都是大多数算法优化。即使在机器语言中创建完美的优化,仍只能将可能输入的限制移动一小部分。除非有人找到真正的算法改进。 - JohanC
因为 214748365 * 10 > 2^31,这是有符号32位整数的最大值。您可以使用无符号整数来获得双倍。但这并没有太大帮助,因为许多结果需要花费过长时间才能找到。 - JohanC

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