使用更快的算法来找出不能被给定一组数字整除的数字数量

9

我正在尝试解决一个在线评判问题: http://opc.iarcs.org.in/index.php/problems/LEAFEAT

问题简述:

如果我们给定一个整数L和一个由N个整数组成的集合s1,s2,s3..sN,我们需要找出0到L-1之间有多少个数字不被'si'中任何一个整除。

例如,如果我们给定 L = 20S = {3,2,5},则在0到19之间有6个数字不被3、2或5整除。

L <= 1000000000,N <= 20。

我使用了容斥原理来解决这个问题:

/*Let 'T' be the number of integers that are divisible by any of the 'si's in the 
given range*/

for i in range 1 to N
  for all subsets A of length i
    if i is odd then:
      T += 1 + (L-1)/lcm(all the elements of A)
    else
      T -= 1 + (L-1)/lcm(all the elements of A)
return T

这里是解决该问题的我的代码

#include <stdio.h>

int N;
long long int L;
int C[30];

typedef struct{int i, key;}subset_e;
subset_e A[30];
int k;

int gcd(a,b){
int t;
    while(b != 0){
            t = a%b;
            a = b;
            b = t;
    }

    return a;
}

long long int lcm(int a, int b){
    return (a*b)/gcd(a,b);
}

long long int getlcm(int n){
  if(n == 1){
    return A[0].key;
  }
  int  i;
  long long int rlcm = lcm(A[0].key,A[1].key);
  for(i = 2;i < n; i++){
    rlcm = lcm(rlcm,A[i].key);
  }
  return rlcm;
}

int next_subset(int n){
  if(k == n-1 && A[k].i == N-1){
    if(k == 0){
      return 0;
    }
    k--;
  }
  while(k < n-1 && A[k].i == A[k+1].i-1){
    if(k <= 0){
      return 0;
    }
    k--;
  }
  A[k].key = C[A[k].i+1];
  A[k].i++;
  return 1;
}

int main(){
  int i,j,add;
  long long int sum = 0,g,temp;
  scanf("%lld%d",&L,&N);
  for(i = 0;i < N; i++){
    scanf("%d",&C[i]);
  }
  for(i = 1; i <= N; i++){
    add = i%2;
    for(j = 0;j < i; j++){
      A[j].key = C[j];
      A[j].i = j;
    }
    temp = getlcm(i);
    g = 1 + (L-1)/temp;
    if(add){
      sum += g;
    } else {
      sum -= g;
    }
    k = i-1;
    while(next_subset(i)){
      temp = getlcm(i);
      g = 1 + (L-1)/temp;
      if(add){
        sum += g;
      } else {
        sum -= g;
      }
    }
  }
  printf("%lld",L-sum);
  return 0;
}
next_subset(n)函数在数组A中生成大小为n的下一个子集,如果没有子集,则返回0,否则返回1。它基于this stackoverflow问题中接受答案描述的算法。 lcm(a,b)函数返回a和b的最小公倍数。 get_lcm(n)函数返回A中所有元素的最小公倍数。它使用属性:LCM(a,b,c) = LCM(LCM(a,b),c) 当我将问题提交给评判机时,它会出现“超时”的情况。如果我们使用暴力解决方法,只能得到50%的分数。
由于可能有多达2^20个子集,因此我的算法可能很慢,因此我需要更好的算法来解决这个问题。 编辑:

在编辑代码并将函数更改为欧几里得算法后,我得到了一个错误的答案,但我的代码在时间限制内运行。它给出了一个正确的示例测试答案,但对于任何其他测试用例都不正确;这是我在ideone上运行我的代码的链接,第一个输出是正确的,但第二个不正确。

我的解决问题的方法正确吗?如果是的话,那么我在代码中犯了一个错误,我会找到它;否则,有人可以解释一下问题在哪里吗?


1
+1 感谢您为这个网站提供了如此多有趣的问题。开始工作吧! - Theocharis K.
1
lcm和gcd函数应该将a、b作为long long类型的参数。 - nhahtdh
7个回答

2

您也可以尝试更改lcm函数,使用欧几里得算法

int gcd(int a, int b) {
    int t;

    while (b != 0) {
        t = b;
        b = a % t;
        a = t;
    }

    return a;
}

int lcm(int a, int b) {
    return (a * b) / gcd(a, b);
}

至少在Python中,两者之间的速度差异相当大:

>>> %timeit lcm1(103, 2013)
100000 loops, best of 3: 9.21 us per loop
>>> %timeit lcm2(103, 2013)
1000000 loops, best of 3: 1.02 us per loop

在实现您描述的方法后,我在时间限制内,但是得到了错误的答案,我将编辑问题并添加新代码。 - 2147483647
1
@A.06:请看我对于C语言的翻译答案。它很可能有效。 - Blender
那正是我在你编辑代码之前所做的事情,我甚至将我的变量命名为t。 - 2147483647
1
@A.06: while 循环中的逻辑有些不同(至少乍一看是这样)。你能测试你的实现是否正确吗? - Blender
1
@A.06 我认为错误的答案是因为在0上加了1,但你不需要那个。然后计算L-1-sum,因为只有L-1个数字1 <= k < L - Daniel Fischer
显示剩余2条评论

1
通常情况下,对于 s_i 的子集中的 k,最小公倍数将超过 L,而 k 要远小于 20。因此,您需要提前停止。
可能只需插入
if (temp >= L) {
    break;
}

之后

while(next_subset(i)){
  temp = getlcm(i);

将足够。

此外,如果在s_i中有任何1,则所有数字都可被1整除。

我认为以下方法会更快:

unsigned gcd(unsigned a, unsigned b) {
    unsigned r;
    while(b) {
        r = a%b;
        a = b;
        b = r;
    }
    return a;
}

unsigned recur(unsigned *arr, unsigned len, unsigned idx, unsigned cumul, unsigned bound) {
    if (idx >= len || bound == 0) {
        return bound;
    }
    unsigned i, g, s = arr[idx], result;
    g = s/gcd(cumul,s);
    result = bound/g;
    for(i = idx+1; i < len; ++i) {
        result -= recur(arr, len, i, cumul*g, bound/g);
    }
    return result;
}

unsigned inex(unsigned *arr, unsigned len, unsigned bound) {
    unsigned i, result = bound, t;
    for(i = 0; i < len; ++i) {
        result -= recur(arr, len, i, 1, bound);
    }
    return result;
}

使用以下命令调用它

unsigned S[N] = {...};
inex(S, N, L-1);

您无需在任何地方添加1到0,因为0可被所有数字整除,计算不被任何s_i整除的数字1 <= k < L的数量。


尝试了您的方法后,没有太大的区别,我仍然遇到了超时问题。 - 2147483647
1
啊,可惜。但我甚至没有看过你的lcm实现,如果你使用欧几里得算法,就像Blender建议的那样,平均来说应该会更快。而且我会提出另一种包含排除的方法,我认为这比你的next_subset更快。 - Daniel Fischer

1
创建一个具有L个条目的标志数组。然后标记每个被触摸的叶子节点:
for(each size in list of sizes) {
    length = 0;
    while(length < L) {
        array[length] = TOUCHED;
        length += size;
    }
}

然后找到未被触碰过的叶子:

for(length = 0; length < L; length++) {
    if(array[length] != TOUCHED) { /* Untouched leaf! */ }
}

请注意,此过程不涉及乘法和除法;但您需要高达1 GiB的RAM。如果RAM是一个问题,那么您可以使用位数组(最大120 MiB)。
然而,这只是一个开始,因为有一些重复模式可以复制而不是生成。第一个模式是从0到S1*S2,下一个是从0到S1*S2*S3,下一个是从0到S1*S2*S3*S4等等。
基本上,您可以设置由S1和S2触及的所有值,然后将其从0到S1*S2的模式复制; 然后将该模式复制到S1*S2*S3并设置S3和S1*S2*S3之间的所有S3; 然后继续复制该模式,直到S1*S2*S3*S4并设置S4和S1*S2*S3*S4之间的所有S4等等。
接下来,如果S1*S2*...Sn小于L,则知道该模式将重复,并且可以从该模式生成长度从S1*S2*...Sn到L的结果。在这种情况下,数组的大小只需要是S1*S2*...Sn,而不需要是L。

最后,如果S1*S2*...Sn大于L,则可以生成S1*S2*...(Sn-1)的模式,并使用该模式从S1*S2*...(Sn-1)到S1*S2*...Sn创建结果。在这种情况下,如果S1*S2*...(Sn-1)小于L,则数组不需要像L那样大。


谢谢,但我已经尝试过了,我用了一个位集,但L的大小也没有改变,问题的内存限制是3MB左右。 - 2147483647
好的,让我尝试其他方法! :-) - Brendan

1

您可以跟踪每个大小的下一个触摸叶子的距离。下一个触摸叶子的距离将是最小的距离,您需要从所有其他距离中减去这个距离(并在距离为零时进行包装)。

例如:

 int sizes[4] = {2, 5, 7, 9};
 int distances[4];
 int currentLength = 0;

 for(size = 0 to 3) {
     distances[size] = sizes[size];
 }

 while(currentLength < L) {
     smallest = INT_MAX;
     for(size = 0 to 3) {
         if(distances[size] < smallest) smallest = distances[size];
     }
     for(size = 0 to 3) {
         distances[size] -= smallest;
         if(distances[size] == 0) distances[size] = sizes[size];
     }
     while( (smallest > 1) && (currentLength < L) ) {
         currentLength++;
         printf("%d\n", currentLength;
         smallest--;
     }
 }

1

恐怕您对问题的理解可能不正确。

您有L。您有一个包含K个元素的集合S。您必须计算L / Si的商的总和。对于L = 20,K = 1,S = {5},答案很简单,即16(20-20/5)。但是K> 1,因此您还必须考虑公共倍数。

为什么要循环遍历子集列表?这不涉及子集计算,只涉及除法和乘法。

您有K个不同的整数。每个数字都可以是质数。您必须考虑公共倍数。就这些。

编辑

L = 20,S = {3,2,5}

叶子可以被3 = 6吃掉
叶子可以被2 = 10吃掉
叶子可以被5 = 4吃掉

S的公共倍数,小于L,不在S中= 6、10、15

实际吃掉的叶子= 20/3 + 20/2 + 20/5 - 20/6 - 20/10 - 20/15 = 6


我可以用另一种方法解决这个问题,但它们都涉及循环L次,由于K的大小不够快。 - 2147483647
问题要求您研究基本的整数理论而不是集合理论。循环设计,循环算法并不是重点。 - 9dan
我不明白你的意思,但是如果我们在代码中任何地方循环L次,就会得到TLE错误,我已经尝试过了。 - 2147483647

1

@A.06:您在opc上的用户名是linkinmew,对吗?

无论如何,答案只需要您生成所有可能的子集,然后应用容斥原理即可。这将完全符合所给数据的时间限制。为了生成所有可能的子集,您可以轻松地定义一个递归函数。


1
希望在询问之前,你能尽力自己解决问题,因为你已经问了几乎每一个好的问题关于opc。干杯! - Somebody

0

我不懂编程,但在数学中有一个定理适用于具有GCD 1的集合 L=20,S=(3,2,5) (1-1/p)(1-1/q)(1-1/r).....等等 (1-1/3)(1-1/2)(1-1/5)=(2/3)(1/2)(4/5)=4/15 4/15表示在每组15个数字中有4个数字不能被任何其他数字整除,其余数字可以手动计算,例如: 16、17、18、19、20(只有17和19意味着只有2个数字不能被任何S整除) 4+2=6 6/20表示在前20个数字中只有6个数字不能被任何S整除


1
考虑将数学部分从您的答案中分离出来,使其更易读(例如,拆分为多行、段落)。 - rudolfovic

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