【问题标题】:Faster algorithm to find how many numbers are not divisible by a given set of numbers更快的算法来找出有多少数字不能被给定的一组数字整除
【发布时间】:2013-01-13 03:51:28
【问题描述】:

我正在尝试解决在线法官问题: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

我使用了Inclusion-Exclusion原理来解决这个问题:

/*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 的 lcm。 get_lcm(n) 函数返回A 中所有元素的lcm。 它使用属性:LCM(a,b,c) = LCM(LCM(a,b),c)

当我将问题提交给法官时,它会给我一个“超出时间限制”。如果我们用蛮力解决这个问题,我们只能得到 50% 的分数。

由于最多可以有 2^20 个子集,我的算法可能会很慢,因此我需要一个更好的算法来解决这个问题。

编辑:

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

我解决这个问题的方法正确吗?如果是,那么我在代码中犯了一个错误,我会找到它;否则任何人都可以解释什么是错的吗?

【问题讨论】:

  • +1 为这个网站提供了这么多有趣的问题。去工作!
  • lcm 和 gcd 函数应该占用 a、b 一样长的时间。

标签: c algorithm subset lcm


【解决方案1】:

您也可以尝试更改您的 lcm 函数以使用 Euclidean algorithm

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

【讨论】:

  • 实现你描述的方法后,我在时限内但我得到了错误的答案,我将编辑问题并添加新代码
  • @A.06:请参阅我的 C 翻译答案。它可能会起作用。
  • 这和我在你编辑你的代码之前做的一模一样,我什至把我的变量命名为 t。
  • @A.06:while 循环中的逻辑有点不同(至少乍一看是这样)。你能测试一下你的实现是否正确吗?
  • @A.06 我认为错误的答案是由于为 0 添加了1,但您不需要它。然后计算L-1 - sum,因为只有L-1 数字1 &lt;= k &lt; L
【解决方案2】:

通常情况下,s_ik 子集的最小公倍数将超过 L,因为 k 远小于 20。因此您需要尽早停止。

可能只是插入

if (temp >= L) {
    break;
}

之后

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

足够了。

另外,如果s_i中有1s,则所有数字都可以被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);

您无需在任何地方为 0 添加 1,因为 0 可以被所有数字整除,请计算不能被任何 s_i 整除的数字 1 &lt;= k &lt; L 的计数。

【讨论】:

  • 试了你的方法后,没有太大区别,我还是得到了Time Limit Exceeded
  • 啊,可惜了。但是我什至没有查看您的lcm 实现,如果您使用欧几里得算法,正如Blender 所建议的那样,这应该会使其平均速度显着加快。我会建议另一种包含排除的方法,我认为这会比你的next_subset更快。
【解决方案3】:

创建一个包含 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;然后将模式从 0 复制到 S1*S2 直到到达 S1*S2*S3 并设置 S3 和 S1*S2*S3 之间的所有 S3;然后复制该模式,直到到达 S1*S2*S3*S4 并将所有 S4 设置在 S4 和 S1*S2*S3*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 一样大。

【讨论】:

  • 谢谢,但我也试过了,我用 bitset 做的,但是 L 的大小也不是问题的内存限制是 3mb 什么的
  • 嘿-好的-那我试试别的吧! :-)
【解决方案4】:

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

你有 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 的大小,速度不够快。
  • 问题希望你研究基本的整数理论而不是集合论。循环设计,循环算法不是重点。
  • 我不明白你的意思,但是如果我们在代码中的任何地方循环 L 次,我们会得到一个 TLE,我已经尝试过了。
【解决方案5】:

您可以跟踪每个尺寸的下一个接触叶子的距离。到下一个接触的叶子的距离将是恰好是最小的距离,并且您将从所有其他距离中减去此距离(并在距离为零时回绕)。

例如:

 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--;
     }
 }

【讨论】:

    【解决方案6】:

    @A.06:你是在 opc 上使用用户名 linkinmew 的那个吗?

    无论如何,答案只需要你制作所有可能的子集,然后应用包含排除原则。这将完全在给定数据的时间范围内。为了生成所有可能的子集,您可以轻松定义递归函数。

    【讨论】:

    • 我希望你在问之前尽量自己解决问题,因为你已经问过几乎所有关于 opc 的好问题。干杯!
    【解决方案7】:

    我不知道编程,但在数学中有一个定理适用于具有 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 整除

    【讨论】:

    • 考虑将数学从答案中分离出来,使其更具可读性(例如,分成多行、多段)。
    猜你喜欢
    • 2020-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多