zoukankan      html  css  js  c++  java
  • 个人总结的OI、ACM竞赛编码技巧:调试,查错,常数优化

    技巧1:封装一个能进行越界检查的Array类

    C/C++中数组越界是一件很让人头疼的事。内置的数组类型不进行越界检查,而越界后的行为是未定义的。这就导致有时候程序的行为极其诡异,程序员却迟迟定位不了bug的位置。

    C++11提供的std::array类提供了at()成员函数,该函数会在下标越界时抛出一个std::out_of_range类型的异常。std::array重载的operator []不进行越界检查。

    但有时我们既想要越界检查,又不想牺牲性能,还不想改变用operator []访问元素的写法。为此我们对数组进行一些封装:

     1 #include <array>
     2 template <class Tp, size_t N>
     3 struct Arr: public std::array<Tp, N> 
     4 {
     5 #ifndef NDEBUG
     6     Tp& operator [] (size_t idx) { return this->at(idx); }
     7     const Tp& operator [] (size_t idx) const { return this->at(idx); }
     8 #endif 
     9 };
    10 template <class Tp, size_t N1, size_t N2>
    11 using Arr2 = Arr<Arr<Tp, N2>, N1>;

    这里的Arr类继承了std::array的所有成员。在NDEBUG宏未定义时,派生类Arr的operator []覆盖了基类的版本。(顺便黑一波C++坑出风采的const机制)

    不需要越界检查时,只需在前边加一句#define NDEBUG即可。(-O2和-O3编译优化下会自动添加NDEBUG,以禁用assert宏)

    最后两行是二维数组的一个typedef。注意第11行里边,第二维的大小N2在里层,第一维的大小N1在外层。

    在OI、ACM等比赛中,常数优化虽然不像复杂度优化那样重要,但对于某些数据量很大的题目,如果实现时常数太大,也会有TLE的风险。

    不过也要注意,常数优化往往是以程序可读性降低为代价的,在实际编程时需要有所取舍。(更何况卡常数的题很容易被喷成毒瘤¬_¬)

    技巧2:arr[x +1] = f(arr[x])的时间常数通常比arr[x] = f(arr[x - 1])更小

    举一个简单的例子:

     1 using LL = long long;
     2 LL arr[50000000];
     3 void foo() {
     4     for (int i = 1; i < size(arr); i++)
     5         arr[i] = i + arr[i - 1];
     6 }
     7 void bar() {
     8     for (int i = 0; i < size(arr) - 1; i++)
     9         arr[i + 1] = i + 1 + arr[i];
    10 }

    两种递推的作用是完全等效的,不过对于CPU而言,前一种写法更为“缓存友好”(cache friendly)。当访问arr[i]时,CPU会将紧随其后的一小块缓存起来,访问CPU缓存的速度要快于访问内存。

    关于“缓存友好”的详细内容可以参考:https://stackoverflow.com/questions/16699247/what-is-cache-friendly-code

    测试程序以及运行结果如下:(编译参数:-std=c++17 -O2)

     1 #include <iostream>
     2 #include <utility>
     3 #include <boost/timer.hpp>
     4 
     5 using namespace std;
     6 using LL = long long;
     7 LL arr[50'000'000];
     8 void foo() {
     9     for (int i = 1; i < size(arr); i++)
    10         arr[i] = i + arr[i - 1];
    11 }
    12 void bar() {
    13     for (int i = 0; i < size(arr) - 1; i++)
    14         arr[i + 1] = i + 1 + arr[i];
    15 }
    16 
    17 int main()
    18 {
    19     for (int i = 0; i < 5; i++)
    20     {
    21         cout << "Case " << i + 1 << ":
    ";
    22         boost::timer timer;
    23         foo();
    24         cout << "foo(): " << timer.elapsed() << '
    ';
    25         timer.restart();
    26         bar();
    27         cout << "bar(): " << timer.elapsed() << "
    ";
    28     }
    29 }
    30 
    31 /*
    32 Case 1:
    33 foo(): 0.227
    34 bar(): 0.073
    35 Case 2:
    36 foo(): 0.099
    37 bar(): 0.077
    38 Case 3:
    39 foo(): 0.1
    40 bar(): 0.074
    41 Case 4:
    42 foo(): 0.102
    43 bar(): 0.075
    44 Case 5:
    45 foo(): 0.103
    46 bar(): 0.079
    47 */

    可以看到对于long long类型,bar()的运行时间比foo()短了大约20%(Case 1的数据比较诡异,第一次运行的话耗时都会很长,求大神解释)。

    而对于int类型,测试结果表明bar()的运行时间比foo()短了50%~60%,优势更为突出。

    ——To be continued——

  • 相关阅读:
    ListView的Item被点击和其中的Button被点击同时生效
    Android Canvas绘图详解(图文)
    Android--获取当前系统的语言环境
    自定义seekbar中,thumb被覆盖掉一部分问题
    Android类参考---Fragment
    使用LocalBroadcastManager
    寒哥细谈之AutoLayout全解
    Xcode6中自动布局autolayout和sizeclass的使用
    Swift 2.0初探:值得注意的新特性
    swift调用相机和相册
  • 原文地址:https://www.cnblogs.com/Onlynagesha/p/8645585.html
Copyright © 2011-2022 走看看