【问题标题】:how to make the execution time less(ie. a faster code) for this problem如何减少此问题的执行时间(即更快的代码)
【发布时间】:2011-04-08 10:12:19
【问题描述】:

这个问题来自 Codechef.com [如果有人还在解决这个问题,请不要在尝试自己之前进一步查看帖子] 虽然它运行正确,但我需要让它更快。我是 c 的初学者,c ++。(我知道数组,字符串和指针,但不知道文件处理等)。所以有没有办法让这个程序运行得更快,而不会使它变得复杂(如果它的算法复杂就可以了)。 如果您提及您所遵循的所有书籍,我也会接受复杂的编码:)。 我目前正在关注罗伯特·拉福尔。 这是程序:-

有 N 个数 a[0],a[1]..a[N - 1]。最初都是 0。你必须执行两种类型的操作:

1) 将索引 A 和 B 之间的数字加 1。这由命令“0 A B”表示

2) 回答索引 A 和 B 之间有多少个数可以被 3 整除。这由命令“1 A B”表示。

输入:

第一行包含两个整数,N 和 Q。接下来的 Q 行中的每一行都是上面提到的“0 A B”或“1 A B”的形式。

输出:

为“1 A B”形式的每个查询输出 1 行,其中包含相应查询所需的答案。

示例输入:

4 7
1 0 3
0 1 2
0 1 3
1 0 0
0 0 3
1 3 3
1 0 3

样本输出:

4
1
0
2

约束:

1 <= N <= 100000
1 <= Q <= 100000
0 <= A <= B <= N - 1

这是我的解决方案:-

#include<stdio.h>

int main()
{
    unsigned int n; //amount of numbers taken
    scanf("%u",&n);

    unsigned int arr[n],q,a,b,count=0,j,i;
    short int c;
    scanf("%u",&q);//cases taken  
    for(i=0;i<n;++i)
    {
      arr[i]=0;
    }    


    for(i=0;i<q;++i)
    {
      scanf("%d%u%u",&c,&a,&b);//here a,b are A and B respectively and c is either 
                               //o or 1

      if(c==0)
      {
        for(j=a;j<=b;++j)
          arr[j]++;
      }


      if(c==1)
      {
        for(j=a;j<=b;++j)
        {
          if(arr[j]%3==0)
            count++;
        }
       printf("%u\n",count);
      }
      count=0;
    }  

}

【问题讨论】:

  • 为什么我感觉不到一周前就出现了同样的问题?
  • 不,正如我所说,这是 codechef.com 的问题。它首先出现在 codechef 的 9 月 1 日。所以它不可能回来超过一周
  • @gamma:我确定是其他人发布的。这是一个非常普遍的问题。很多其他的编程竞赛网站肯定会有这个问题。
  • 此代码仅在使用 ISO C99 编译时有效(排除使用 VC++)。我至少建议让它更普遍地便携。
  • @Clifford。如果微软懒得支持一个 10 多年前的标准,那真的会阻碍世界其他地区吗?

标签: c++ c time performance execution


【解决方案1】:

两个非常简单的优化:

您实际上只能将值模 3 存储在数组中,而不是实际值。

增量可以通过一个简单的查找表来完成(避免比较和分支):

char increment[3] = { 1, 2 ,0 };
new_val = increment[old_val];

测试 3 整除性现在与 0 进行比较 - 这比整数除法快得多。

【讨论】:

  • 我认为现代 CPU 更喜欢 new_val = (old_val + 1) == 3 ? 0 : (old_value+1) 而不是额外的查找表。但是无论如何,您都想创建查找表static,这样它就不会在堆栈上,而是在数据段中。
  • @Dummy00001 - 不知道为什么现代 CPU 更喜欢算术运算和分支而不是缓存内存读取。关于static - 你可能是对的,尽管它只有 3 个字节,这并不重要。
  • 首先,算术运算比缓存内存读取要快 - 尤其是如果事先不知道地址。其次,有了一个好的编译器,这里就没有分支——只有一个条件移动。
【解决方案2】:

这很复杂,但请跟我来。除了“27 年的编码经验”之外,我无法引用任何具体的来源。

最初的问题是将数轴设置为自然整数 0,1,2,3,4,5,6... 但是,我们只关心能被 3 整除的数字,所以让我们将数轴重新定义为其中只有三个值:{2,3,4} 并重新映射数字线:

0 => 4
1 => 2
2 => 3
3 => 4
4 => 2
5 => 3
6 => 4
..等等。

您会注意到可被 3 整除的数字在我们的序列中映射为 4。为什么使用 {2,3,4}? 4 在二进制中是 100,这意味着任何设置了第 3 位的元素都可以被 3 整除。这很容易用位运算测试。

由于我们使用 2,3,4 作为三进制序列,我们可以将数组元素大小减少到 4 位。我们将数组定义为 8 位值,但大小为我们需要的字节大小的一半(如果它是奇数大小的数组,则加 1),并将每个字节的元素存储在数组中。添加和比较可以作为 SIMD(单指令,多数据)操作完成,通过使用一些巧妙的位操作,每次循环迭代增加或检查多达 16 个元素。

就是这个概念。现在开始代码。

首先,我们需要分配和初始化我们的数组。

unsigned char *arr = malloc(n/2 + 1);

// Init all element values to 4:
memset(&arr, 0x44, n/2 + 1);

我们将通过将 8 个数组字节的块转换为 uint_64 来一次增加 16 个元素,添加 0x1111111111111111 然后跳到下一个块。重复 32 位、16 位、8 位和 4 位数学运算,在运算结束时最多剩余 8、4、2 或 1 个。

在每次递增之前,任何值为 4 的值都需要在递增之前递减 3,以将数字保持在正确的位置。

这是增量命令的代码(未经测试):

/**
   @param p
      p is the address of the byte with the first aligned element to be incremented, &arr[A/2] when A is even, &arr[A/2]+1 when A is odd.
   @param j
      j is the number of elements to increment.  (B-A) when A is even, (B-A-1) when A is odd.
 */ 
void increment_aligned_block(unsigned char *p, int j)
    uint64_t fours;

    while (j>16) {
       // Find the ones that are value 4
       fours = *p & 0x4444444444444444;
       // Decrement each that matches by 3
       *p -= (fours >> 1 | fours >> 2);

       // Add 1 to each of the 16 array elements in the block.
       (uint64_t)(*p) += 0x1111111111111111;
       p += 8; j -= 16;
    }
    if (j >= 8) {
        // repeat the above for 32-bits (8 elements)
        // left as an exercise for the reader.
        p += 4; j -= 8;
   }
    if (j >= 4) {
        // repeat the above for 16-bits (4 elements)
        // left as an exercise for the reader.
        p += 2; j -= 4;
    }
    if (j >= 2) {
        // repeat the above for 8-bits (2 elements)
        // left as an exercise for the reader.
        p += 1; j -= 2;
    }
    if (j == 1) {
        // repeat the above for 8-bits (1 elements)
        // left as an exercise for the reader.
    }
}

比较用:

/**
   @param p
      p is the address of the byte with the first aligned element to be counted, &arr[A/2] when A is even, &arr[A/2]+1 when A is odd.
   @param j
      j is the number of elements to count.  (B-A) when A is even, (B-A-1) when A is odd.
 */ 
int count_aligned_block(unsigned char *p, int j)
    int count = 0;
    uint64_t divisible_map;

    while (j > 16) {
        // Find the values of 4 in the block
        divisible_map = (uint64_t)(*p) & 0x4444444444444444;

        // Count the number of 4s in the block,
        // 8-bits at a time
        while (divisible_map) {
          switch (divisible_map & 0x44) {
            case 0x04:
            case 0x40:
                count++;
                break;
            case 0x44:
                count += 2;
                break;
            default:
                break;
          }
          divisible_map >>= 8;
        }
    }
    // Repeat as above with 32, 16, 8 and 4-bit math.
    // Left as an exercise to the reader

    return count;
}

您可能已经注意到函数被称为foo_aligned_blockp 需要是第一个对齐元素的字节。那是什么?由于我们每个字节打包两个元素,因此起始元素索引必须与偶数对齐。如果文件中的命令是0 0 30,那么我们可以调用increment_algined_block(&amp;arr[A/2], 30),没问题。但是,如果文件中的命令是0 1 30,那么我们需要额外的代码来处理索引1处未对齐的第一个元素,然后调用increment_aligned_block(&amp;arr[A/2 + 1], 29)。同样,作为练习留给读者。


我想指出,这不是最优化的。

未对齐的访问通常非常昂贵。也就是说,从 8 字节对齐地址读取 8 字节值比从非对齐地址读取要快。我们可以添加额外的优化以仅调用 foo_aligned_block() 以保证所有访问都对齐。

【讨论】:

  • 谢谢...你能告诉我 0x4444444444444444 和 0x1111111111111111 是什么。我的意思是我已经彻底搜索了我的书,但我似乎无法在任何地方找到这样的操作。不过我确实得到了你的算法。
  • @gamma:这些是位掩码,用于从位图中仅提取一些位。将它们以二进制形式写出来,然后按位 AND 看看会发生什么。
  • 最好使用 {0,1,2,(3)} 序列。然后,您可以仅将模数拟合为两位。虽然“3”的测试稍微复杂和昂贵,但这被将 TWICE 尽可能多的模数放在一个单词中的优势大大抵消了。请参阅我在 Dummy00001 的答案下的第二条评论。
  • @slacker:谢谢,我会调查一下
【解决方案3】:

1) 将索引 A 和 B 之间的数字加 1。这由命令“0 A B”表示

2) 回答索引 A 和 B 之间有多少个数可以被 3 整除。这由命令“1 A B”表示。

最初的数字是 0,因此可以被 3 整除。加一使数字不可整除。下一个增量 - 数字仍然不可分割。第三个增量使数字再次可整除。

可以尝试的第一个优化是不要让数字增长到 2 以上:如果在增量期间数字从 2 变为 3,则将其设置回零。现在搜索范围成为与 0 的简单比较。(这样数组将包含而不是其模 3 的数字。)

第二个优化是使用范围而不是普通数组,例如类似于RLE 的东西:折叠到一个范围内,所有相邻的数字具有相同的可除性。数组将包含如下结构,而不是数字:

struct extent {
   int start; /* 0 .. N-1; extent should have at least one number */
   int end;   /* 0 .. N   */
   int n;     /* 0, 1, 2; we are only interested in the result of % */
};

最初,该数组将包含覆盖所有数字{0, N, 0} 的单个范围。在增量步骤期间,范围可能会被分割或与相邻的范围合并。这种表示将加快数字的计数,因为您不是一个接一个地遍历数组,而是以块的形式遍历数组。 (如果所有范围都只包含一个元素,它仍然会降级为线性搜索。)


另一种方法是使用具有索引的三个集合来代替数组。 Set #0 将包含模 3 为 0 的数字的所有索引,设置 #1 - 1,设置 #2 - 2。由于在递增操作期间,我们需要进行搜索,而不是 std::set 最好使用例如std::bitset 每一位都标记了属于该集合的数字的索引。

注意这样我们就根本不保留原始数字。我们隐式只保留模 3 的结果。

在增量过程中,我们需要找到索引属于哪个集合,例如设置#n,并将索引移动到下一个(mod 3)集合:将集合n中的位设置为零,将集合n + 1 (mod 3)中的位设置为1。现在计算可被 3 整除的数字就像计算集合 #0 中的非零位一样简单。这可以通过创建一个 temp std::bitset 作为掩码来实现,其中[A,B] 范围内的位设置为 1,使用 temp 集进行掩码设置 #0 并在结果位集上调用 std::bitset::count()

【讨论】:

  • 或者您可以同时使用这两种方法并在运行时选择最好的一种:具有一个范围结构,具有两种类型的范围 - 一个“平面”范围(您描述的那个,所有元素共享一个共同的模数) 和一个“数组”范围,其中元素具有不同的模数,这些模数分别为每个元素存储。您从覆盖整个范围的单个“平坦”范围开始,当您获得一系列非常短的“平坦”范围时,您将其转换为单个“数组”范围。这应该会显着改善最坏情况下的性能。
  • 您可以通过利用模数适合两个位的事实来优化“数组”方法 - 因此将其中的 16 个存储在一个 32 位整数中。将0x55555555 添加到整数以在单个操作中增加所有模数。将结果保持在array &amp;= ~( (array &gt;&gt; 1 &amp; array &amp; 0x55555555) * 3) 的范围内。
  • 实际上,在我的脑海中,我更多地考虑摆脱O(n) 的复杂性。 Bit tricks are plenty. 问题是如何打破O(n) 在计数和递增操作上的最坏情况,比如说在O(log(n)) 上。但这似乎是不可能的。
  • @gamma:诀窍是从您的任务和可用算法的角度考虑数据。学习数学(应用代数或数学分析等无聊的东西)和调整思维以适应任务的技巧变得非常容易。对于范围,您可能需要检查 B-treestries
  • 如果我没记错的话,你在数据结构中学到了这一点,对吧?该死的我知道我应该学习计算机科学.......但是电子产品仍然更好(我可以双向做)。感谢您的建议,现在我知道我缺少什么了。
【解决方案4】:

您可以进行的一项改进是替换

if (c == 0) {
    //code here
}

if (c == 1) {
   // code here
}

与:

if (c == 0) {
   //...
} else if (c == 1) {
  //...
}

如果您确定 c 将始终为 0 或 1,您也可以将 else if 替换为简单的 else

真正让您慢下来的是 I/O。如果它是一个足够大的列表,它可能会支付给malloc 足够的内存来保存输入和输出。然后在进入循环之前收集所有输入,并在最后显示输出。

【讨论】:

  • 使用流式 IO 类 (cin) 是否有助于提高 IO 性能?我没有比较它们的经验。
  • 尽管大多数现代编译器应该足够聪明,可以自行计算出else if 并自动进行优化。
  • @JBRWilkinson 不。也许有一点,但只是非常轻微。 IO 很慢但是你这样做。 @格兰特彼得斯。没错,但else if 也让阅读代码的人更清楚。无论如何,IO 是最大的障碍。
  • 否则,如果没有帮助,我的时间仍然超过限制。你能告诉我如何在进入循环之前收集所有输入并在末尾显示输出。它与缓冲区或其他东西有关吗?
【解决方案5】:

这对我来说看起来很高效。 我看到的一件事是您正在使用variable length arrays,这对于 C (AFAIK) 是可以的,但在 C++ 中是非法的,对于 C++,您需要在数组上使用 std::vectornew

我可以看到您提高性能的唯一地方是使用 Duff's Device 进行部分循环展开,我不建议将其用于玩具样本。

【讨论】:

    【解决方案6】:

    我认为除了缺少任何边界检查之外,您的解决方案似乎还不错。在检查“c”或开关时可能使用“else”,但这将为您节省微不足道的时间。 我认为你不会在任何书中找到像这样无用的东西。

    【讨论】:

      【解决方案7】:

      据我所知,您的代码已经过优化。

      -亚历克斯

      【讨论】:

      • 对于一个朴素的算法,即;)。
      猜你喜欢
      • 2021-12-02
      • 1970-01-01
      • 2019-12-14
      • 2016-07-24
      • 1970-01-01
      • 2015-05-12
      • 1970-01-01
      • 2021-06-03
      相关资源
      最近更新 更多