连上前不久的某道考试题,以及今天的T3,被集合这个东西整疯…
记一些以后大概还会用到的知识点orz
1.枚举子集
因为众所周知的原因,正确枚举所有集合的子集的姿势是O(3n)。
感性理解一下,从全集U里拿出一个集合A,然后枚举它的子集B,每个元素只有三种状态:在U中而不在A中,在A中但不在B中,在B中。
理性分析的话,考虑A包含i个1的话,需要的复杂度是2i,再考虑有i个1的集合的情况,总情况就是∑C(n,i)*2i。在每一项的后面补上1(n-i),由二项式定理可得O(3n)。
代码肯定不是枚举每一个A,再老老实实用2n的方式枚举出B。
for(int B=(A-1)&A;B;B=(B-1)&A)
含义是,&A忽略B-1后退位产生的1,相当于前面的1按顺序被枚举掉,然后再回到去掉高位的1的集合状态…这样枚举子集是从大到小的。
2.从小到大枚举子集
上面的枚举顺序是从大到小,那么每一次取个补集就是从小到大啦。
3.向点集中加边(统计每个点集的边数)
一开始的写法是每读入一条边,就枚举所有点集往里塞。复杂度是O(m2n)。
学到的新写法:
for(int i=1,x,y;i<=m;i++){ scanf("%d%d",&x,&y); e[(1<<(x-1))|(1<<(y-1))]++; } for(int i=1;i<=n;i++){ for(int j=1;j<maxn;j++){ if(j&(1<<(i-1)))e[j]+=e[j^(1<<(i-1))]; } }
先把所有的边存进最小的集合,然后进行一次O(n2n)的处理。底下的处理是每次让每个含有当前点的集合加上抠去当前点以后的点集所存的边,相当于每一次把所有边集对应的点集增加1,可以保证不重不漏。
4.
刚刚写这篇的时候,我的代码在卡常带师们的手里焕然一新……从9000+ms跑到了3000-ms,快了三分之二……
于是记一些卡常的东西,这个和集合关系不大。不擅长卡常丢了多少分是数不清的
于是就有诸如:模数用int存,集合的最大状态提前算出【这个原来我一直都没在写的吗喂】,不用担心爆负数于是极限取模【省去+mod的负数处理】,提前算出2的幂【原来我一直也没在写这个吗】等等…