《做题记录》之心得

做题记录

写在一切前面

好像博客weblog的原意是“web log”,网络日志。做题时电脑换来换去太麻烦了,我选择开一个log记录做题时那些该留到交题以后的东西。

常数优化的原则

转自大佬发的文件。好像CSDN上有?不过搞不清原作者是谁了,我在原作的基础上做了几乎全面的修订,如有其它补充和注解或版权问题请在评论区留言。

0. O2/O3。哈哈
#progma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")

这是使用宏打开O2/O3的方式。NOIP及更高等级的比赛默认(在编译命令中)打开O2优化,手动打开属于违规。
这是CCF帮你打开O2的方式:

gcc [name].cpp -o [name] -O2 -std=c++14 -static

-O2启用O2优化,-std=c++14启用C++14标准,-static将静态库(你可以认为是头文件)里所有用到的函数、类、定义等拷贝到你编译出来的程序中。这个命令在至少在Dev-c++里是默认启用的(其他IDE不清楚)。明明汇编程序远比c++靠近底层,你写的几KB的A+B problem编译出来却是MB级别的exe文件,这就是原因。

另外,O系优化会破坏代码行和编译出来的汇编程序的联系,导致你调试时设置的断点、变量查看、调试命令出现不可预知的错误。所以你只能采用最朴素的调试方式:

void funtion(int x,......)
{
	//一些代码
	double ans;
	//一些代码
	ans=aaa+bbb-ccc/ddd;
	printf("我是x=%d时ans的值!!!:%lf\n",x,ans);
	//一些代码
}

当然,你也可以使用下文中提到的cerr。

1. 快读快写

更新:
在关闭流同步的情况下下进行合适的处理,可以认为流输入输出甚至比一般快读要快。——这个结论尚未得到证明。

#define endl '\n'

ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);

更新的更新:

据称关闭同步的iostream比cstdio要快——这倒是有原因,是scanf等每次调用都会对 %d之类操作符进行分析,而cin直接编译器处理了,原理同宏展开。

关于输出!鲜为人知的是输出的时间复杂度是 O ( n + w ) O(n+w) O(n+w) 的,其中 n n n 是输出数据规模, w w w 是刷新输出流缓冲区的次数—— w w w 的常数与缓冲区的长度成正比,可以说巨大。

众所周知的,关闭流同步可以大大加快cout的速度,为什么呢?因为在流同步打开的情况下,为了确保iostream与cstdio输出是同步的,所以每执行一次cout就刷新一次缓冲区,将数据输出到设备中。

在关闭流同步的情况下,只有以下三种情况下缓冲区会刷新:

  • 程序运行结束;

  • 缓冲区内字符长度达到上限;

  • 执行cerr,endl,ends,flush命令人工刷新缓冲区。

cerr的特性事实上非常方便我们利用。简单来说就是使用 cerr<<x;输出,即使你用了freopen,输出的东西也会出现在屏幕(命令行)上,所以使用cerr的纠错语句甚至可以在评测时保留,除了拖慢时间之外不会影响输出在文件中的答案。另外你的程序遇到死循环了,迟迟无法结束,你本来输出的数据就会堵在缓冲区里出不来。cerr会人工刷新缓冲区,让程序不管运行状态如何,只要执行到cerr这条指令,对应的数据就一定会在屏幕上出现的。

endl有类似的作用——但我们需要它吗?我们想要这个特性的时候会使用cerr的。为了时间考虑,最好用宏定义将所有endl替换为’\n’。

火车头里有,不说了。
不过如果你要读入多种类型的变量,又要使用一些符合周礼的C++有而C没有的特性,可以使用多态

inline bool in(int &t)
{
	t=0;
	int ch;
	ch=getchar();
	while(ch==' '||ch=='\n') ch=getchar();
	if(ch==EOF) return 1;
	while(ch!=' '&&ch!='\n'&&ch!=EOF) t=(t<<1)+(t<<3),t+=ch-'0',ch=getchar();
	return 0;
}
inline bool in(string &t)
{
	t="";
	int ch,len=0;
	ch=getchar();
	while(ch==' '||ch=='\n') ch=getchar();
	if(ch==EOF) return 1;
	while(ch!=' '&&ch!='\n'&&ch!=EOF) t[++len]=ch,ch=getchar();
	return 0;
}

这两个同名函数可以共存,只要它们传入参数的类型不完全相同,编译器就可以根据调用“in”这个名字时传入参数是什么类型判断应该调用哪个名为in的函数。这样的代码在后续维护和debug的时候特别方便!!!

举个例子,你写了一个针对int的函数 f u n fun fun( i n t int int x x x),主程序main()都打好了,这时你才发现在题目能达到的某个数据范围中,你main()里的某些变量可能涉及 d o u b l e double double的范围。

你刚想在 f u n ( x ) fun(x) fun(x)里ctrl+r查找替换 i n t − > d o u b l e int->double int>double,不巧发现自己用了位运算。。。

难道要再写一个 f u n f o r d o u b l e ( x ) funfordouble(x) funfordouble(x),然后把main()里出现的几十个 f u n ( x ) fun(x) fun(x)精准找出其中的一些换成 f u n f o r d o u b l e ( x ) funfordouble(x) funfordouble(x)?在修改的过程会给你的代码埋下无数无法察觉的bug,会在judging的时候带来你调到考试结束都调不完的CE,RE和WA.

这个时候多态性救你一命,你只要写一个 f u n fun fun( d o u b l e double double x x x),多态性会出手帮你解决变量该用哪个 f u n ( x ) fun(x) fun(x)的事情。

2. C++98以后到C++14的新特性

许多人C++98写惯了,不清楚C++98以后引入了什么有用的新特性。我选一些有用的讲一讲。

long long(C++11)&long double(C++99):惊不惊喜,意不意外?long long使用64bit(8个字节)是C++11首次定义的,而相对更早的long double熟悉的却比long long更少。

long double和long int一样有个令人蛋疼的地方,它 通常是10字节,有的是12字节或16字节” ,ANSI只规定sizeof(long double)>=sizeof(double),其余没做要求,导致它用在代码中充满了随机性。。。

auto(C++11):举个例子你就知道它怎么用了。

你想不开,要用multimap维护一个带权图:
……

此外,函数返回类型也可以auto推导(C++14)。

弃用register(C++11):见下。

3. 关于关键字们

auto 上面已经讲了。

inline inline只是“建议”编辑器将函数内嵌到主程序中,而#define是强制宏展开,所以:递归函数用inline屁用没有,多态函数等要用函数特性的函数保留inline,其他函数建议改成#define。举例:

/*inline int lowbit(int x)
{
	return x&(-x);
}*/
#define lowbit(x) (x&(-x))

/*inline void in(int &x)
{
	scanf("%d",&x);
}*/
#define in(x) scanf("%d",&x) //不过没有多态性

奇技淫巧:可以使用__attribute__((always_inline))强制内联

register 也只是“建议”编辑器将变量留在寄存器中。C++11后被弃用。鉴于评测环境不同,可能变成零优化甚至负优化。考试时不推荐使用。

据本人测试,在O2优化下,在循环内定义的循环变量i for(int i=1;i<=n;++i)是肯定会放到寄存器内的。这也是建议在循环内定义循环变量的原因。

4. 速度:++x>x++>x+=1>x=x+1

后缀运算符x++将返回对象的副本,意思是将x++中的x复制一份作为返回值,即使它不会被使用。

而前缀运算符++x可以简单地返回对修改对象的引用&x。**传引用几乎总是比传值要快。**顺便说一句,这也是运算符重载中都传一个const的引用的原因。

inline bool operator <(const dot &x) const
    {
        return w>x.w;
    }

x=x+1有4个流程:(以下所有tmp都是寄存器里的临时值)

tmp1=&x(右),读取右x的地址;

tmp2=*tmp1+1,读取x值并+1;

tmp3=&x(左),读取左x的地址;

*tmp3=tmp2,将值传给左边的x(编译器并不认为左右x的地址相同)。

x+=1有3个流程

tmp1=&x,读取x的地址;

tmp2=*tmp1+1,读取x值并+1;

*tmp1=tmp2,将值传给x(x的地址已经读出)。

x++只有2个流程

tmp1=&x,读取x的地址;

INC tmp1,x值自增1(这是汇编);

5. 不要开bool,所有bool改成char,int是最快的(原因不明)。
6. if()else语句比()?()😦)语句要慢,逗号运算符比分号运算符要快。
7. 数据结构用指针代替数组(个人觉得无关紧要)数组在用方括号时做了一次加法才能取地址!所以在那些计算量超大的数据结构中,你每次都多做了一次加法!!!在64 位系统下是long long相加,效率可想而知。

这一点是错误的,因为使用方括号的数组下标本质上不过是语法糖 a n s = a [ n ] ans=a[n] ans=a[n]在编译中会自动替换成ans=*(a+n)。

这也告诉我们为何数组下标从0开始。int a[10001]的时候,事实上就是在内存中从某个地址&i开始分配连续的10001个空间,再令a=&i。这样a[0]就是访问a对应的地址(a+0就是a本身),最大能访问到a[10000],从0到10000刚好是分配了10001个地址。

在许多人喜欢写for(int i=1;i<=n;++i)的情况下,数据范围为n<=10000时,数组a至少也要开到[10001],就是这个原因。

以及这样的代码:

char[N] str;
cin>>(str+1); 
int len=strlen(str+1);

使用for(int i=1;i<=n;++i)进行字符串处理时一定要这么写。使用cin输入字符数组时

//尾递归优化

8. 二进制枚举法

对暴力有奇效。
不过可以使用更巧妙的二进制枚举+状态压缩:

void enumerate(int n)
{
	int i=1,h=(1<<(n+1))-1; //h的二进制表达为111...1一共n个1 
	while(i<=h)
	{
		for(int j=1;j<=n;++j)
		{
			if((i>>(j-1))&1) printf("%d ",j); //i>>(j-1)是i二进制下的第j位,&1代表只有该位为1整个表达式才为真 
		}
		printf("\n");
		++i;
	}
}

9. 尝试去自己重新实现库函数。(不绝对)
isdigit()
max()/min()
unique()/lower_bound()/upper_bound()
scanf()/printf()
cin/cout
getchar()/putchar()
STL::queue/stack/priority_queue/deque
…

提示:memset()效率远高于手写赋值。但memset()原理是给每个目标内存的字节赋上相同的值,所以差不多只有以下4种可用的memset:

memset(a,0,sizeof(a)); //清空
memset(a,-1,sizeof(a)); //变为-1
memset(a,0x3f,sizeof(a)); //变为相加不会溢出的无穷大
memset(a,0x80,sizeof(a)); //变为负无穷大

//isdigit 函数会快于手写判断。

10. 位运算
x10 <=> (x<<3)+(x<<1)
x!=y <=> x^y
x!=-1 <=> ~x
x2 <=> x<<1
x*2+1 <=> x<<1|1
x/2 <=> x>>1
(x+1)%2 <=> x^1
x%2 <=> x&1
x%2==0 <=> !(x&1)
11. strlen()与vector等结构体的.size()
像下面这种代码复杂度是o(nL)的,L为str的长度。
for(int i=0;i<strlen(str);i++)
已经有很多人还是写的上面的这种代码却一直不知情。等你被卡了就知道了。
12. 256M内存的话,int数组最多可以开到 5 * 10^7.

下面的不是绝对,环境不同可能不会出错。
建议平时在编译的时候把编译指令加上 “-ansi”
ANSI C标准都是89年的了,大人时代变了!

信息学竞赛的一些注意事项:[有误请指正]

//cin与scanf 当然有了快读可以都不要。

//分支预测

//define 减少维护难度
编程时利用宏可以减少代码量,但是请务必在每个变量里加括号。 eg. #define rep(i,s,t) for(int i=(s);i<=(t);i++)
以及表达式也要在最外面加括号!!! #define lowbit(x) (x&(-x))

多维数组请把大的放前面。eg. d p [ 10000 ] [ 10 ] [ 2 ] dp[10000][10][2] dp[10000][10][2] 而不是 d p [ 2 ] [ 10 ] [ 10000 ] dp[2][10][10000] dp[2][10][10000],常数差距0.5s。比算法的差距还大。

pow()函数请慎用,低版本有的时候会CE。

二分图匹配避免link做变量名(还有个什么变量名Linux也会CE我突然记不到了…
有时其实也可以用**“中国式的变量名命名法”**这样不会CE。
不推荐这种诡异的风格),Linux环境可能会CE。

少用“math.h”|“cmath”库。因为_x,_y,y1,y2,x1,x2,x0,y0,这类命名有时会CE。

考场严禁使用带下划线的库函数。eg. __gcd()
//2021.9.1 《关于 NOI 系列活动中编程语言使用限制的补充说明》允许使用以下划线开头的库函数或宏(但具有明确禁止操作的库函数和宏除外)
//__int128
//pb_ds库
//unordered_map

编程时利用宏可以减少代码量,但是请务必在每个变量里加括号。 eg. #define rep(i,s,t) for(int i=(s);i<=(t);i++)

循环变量for(int i;…;…;)请不要放到全局上。这种常数不会卡。相反会带来很多隐式的错误

请熟悉STL里面的 string queue stack vector set map 后面这些用的少,仅供参考并且在pascal选手消失前应该是不会考的
2022年后CSP-S将不可使用Pascal、C语言,只能使用C++。
原贴作者不幸预言成功。

deque multiset multimap bitset 这些只是方便才用,但请注意常数!
~~推荐自己实现。~~年轻的少年呦,为什么不看看__gnu_pbds和__gnu_cxx呢?
虽然
这里顺便讲一个namespace的问题。上面两个下划线库里

宏指令少用,#progma 肯定是禁了的,别想手动扩栈。涉及操作编译器和系统的函数都要挂。

内嵌汇编也是算作弊处理,毕竟这是算法竞赛,不是信息安全竞赛,也不是编程能力竞赛。

文章最后插播一个小小的笑料:说到“不是信息安全竞赛”,NOIP2020中某位选手通过修改输入文件hack了第三题的spj

但理论上这位选手的程序行为并未违反CCF关于NOI系列赛编程语言使用限制的规定,CCF要求选手“打开或创建题目规定的输入/输出文件之外的其它文件和目录”,可选手只不过简单写了一条freopen(“ball.in”,“w”,stdout)用测试样例覆盖输入,再将测试输出作为输出……

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值