更快的算法,用于查找一组给定数字不能分割的数量

时间:2013-01-13 03:51:29

标签: c algorithm subset lcm

我正在尝试解决在线裁判问题: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的lcm。 get_lcm(n)函数返回A中所有元素的lcm。 它使用属性:LCM(a,b,c) = LCM(LCM(a,b),c)

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

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

修改

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

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

7 个答案:

答案 0 :(得分:2)

您也可以尝试更改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

答案 1 :(得分:1)

通常情况下,k的{​​{1}}子集的最低公倍数将超过s_i L远小于20,因此您需要提前停止。

可能只需插入

k

if (temp >= L) {
    break;
}

就足够了。

此外,如果while(next_subset(i)){ temp = getlcm(i); 中有1个,则快捷方式,所有数字都可以被1整除。

我认为以下内容会更快:

s_i

调用它
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;
}

您无需在任何地方为0添加1,因为0可被所有数字整除,计算不能被任何unsigned S[N] = {...}; inex(S, N, L-1); 整除的数字1 <= k < L的数量。

答案 2 :(得分:1)

我担心你的问题理解可能不正确。

你有L.你有一组K元素。你必须计算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

答案 3 :(得分: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;然后将模式从0复制到S1 * S2,直到进入S1 * S2 * S3并将所有S3设置在S3和S1 * S2 * 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一样大。

答案 4 :(得分: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--;
     }
 }

答案 5 :(得分:1)

@ A.06:你用opc上的用户名linkinmew,rite?

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

答案 6 :(得分: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 整除