提前优化是万恶之源。
什么叫做真正的代码能力?一直以来我都觉得自己的代码能力不错,直到我碰到了这道题:
POJ 3321 Apple Tree
给一棵树,操作一改变一个点的权值,操作二求以某个点为根的子树的权值和。
(见【笔记】dfs序,欧拉序,LCA的RMQ解法)
这道题是我在学习dfs序时碰到的题,解法非常简单:
如果求出了这棵树的dfs序,就可以将问题从树转化为数字序列上的问题。
操作1:单点修改
操作2:区间求和,范围是st[x],ed[x]
使用树状数组维护即可。
以下是我的第一版代码,复制了树状数组模板之后比较快就写完了,
/* LittleFall : Hello! */
#include
using namespace std; typedef long long ll;
inline int read(); inline void write(int x);
const int M = 500016, MOD = 1000000007;
class BinIdTree
{
int n;
vector<int> save;
public:
explicit BinIdTree(int sz = 0) : n(sz) //建立一个空BIT
{
save.assign(n + 1, 0);
}
explicit BinIdTree(const vector<int> &src) : n(src.size() - 1) //由已知数组O(n)建立
{
save.assign(src.begin(), src.end());
for(int i = 1; i <= n; i++) if(i + (i & -i) <= n)
save[i + (i & -i)] += save[i];
}
inline void add(int p, int x) //单点修改
{
for(; p <= n; p += p & -p) save[p] += x;
}
inline int sum(int l, int r) //区间求和
{
return sum(r) - sum(l - 1);
}
inline int sum(int p)
{
int res = 0;
for(; p; p -= p & -p) res += save[p];
return res;
}
};
vector<int> save[M];
int st[M], ed[M];
void dfs(int now, int fa=-1)
{
static int cnt = 0;
st[now] = ++cnt;
for(int i=0;i<(int)save[now].size();i++)
{
int nxt = save[now][i];
if(nxt!=fa) dfs(nxt,now);
}
ed[now] = cnt;
}
int main(void)
{
#ifdef _LITTLEFALL_
freopen("in.txt","r",stdin);
#endif
int n = read();
for(int i=1;i<n;i++)
{
int a = read(), b = read();
save[a].push_back(b);
save[b].push_back(a);
}
dfs(1);
BinIdTree bt(vector<int>(n+1,1));
int q = read();
while(q--)
{
char op[3];
int node;
scanf("%s %d",op,&node);
if(op[0]=='Q')
printf("%d\n",bt.sum(st[node],ed[node]) );
else
{
if(bt.sum(st[node],st[node]))
bt.add(st[node],-1);
else
bt.add(st[node],1);
}
}
return 0;
}
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
inline void write(int x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
}
第一次交,CE,因为POJ不支持bits.
第二次交,TLE,因为POJ不开O2.
当时我想POJ真是糟糕透了,我一度试图在其它的OJ上找到一样的题,然而失败。
挣扎许久后,我改换了写法。
#include
const int M = 100016;
int n;
/*链式前向星-开始*/
int fst[M*2], pnt[M*2], nxt[M*2];
void add(int x, int y)
{
static int tot = 0;
pnt[++tot] = y;
nxt[tot] = fst[x];
fst[x] = tot;
}
/*链式前向星-结束*/
/*dfs序-开始*/
int st[M], ed[M];
void dfs(int now, int fa = 0)
{
static int cnt = 0;
st[now] = ++cnt;
for(int i = fst[now]; i; i = nxt[i])
if(pnt[i] != fa) dfs(pnt[i],now);
ed[now] = cnt;
}
/*dfs序-结束*/
/*树状数组-开始*/
int bit[M];
inline void modify(int p, int x)
{
for(;p<=n;p+=p&-p) bit[p]+=x;
}
inline int sum(int p)
{
int res = 0;
for(;p;p-=p&-p) res += bit[p];
return res;
}
inline int sum(int l, int r)
{
return sum(r) - sum(l-1);
}
/*树状数组-结束*/
int main(void)
{
scanf("%d",&n);
for(int i=1,a,b;i<n;i++)
{
scanf("%d%d",&a,&b);
add(a,b), add(b,a);
}
dfs(1);
for(int i=1;i<=n;i++)
modify(i,1);
int q,node;
char op[3];
scanf("%d",&q);
while(q--)
{
scanf("%s %d",op,&node);
if(op[0]=='Q')
printf("%d\n",sum(st[node],ed[node]) );
else
modify(st[node],sum(st[node],st[node])?-1:1);
}
return 0;
}
这份代码没有使用封装类、没有使用vector、把邻接矩阵改写成了链式前向星,结果是:编译速度快了10倍,运行速度快了8倍,通过了此题。
做完之后,我想了很长时间,到底是POJ糟糕透了,还是我的代码糟糕透了?POJ是没开O2,可是如果之后的哪次比赛卡常,导致就差这一点优化时间而TLE,我也要去抱怨比赛吗?
后来,我把我很多的板子都从vector+封装类换成了简单的数组形式,举个例子,区间修改版的树状数组:
class BinIdTree
{
int n;
vector<ll> save;
public:
explicit BinIdTree(int sz = 0) : n(sz) //建立一个空BIT
{
save.assign(n + 1, 0);
}
explicit BinIdTree(const vector<ll> &src) : n(src.size() - 1) //由已知数组O(n)建立
{
save.assign(src.begin(), src.end());
for(int i = 1; i <= n; i++) if(i + (i & -i) <= n)
save[i + (i & -i)] += save[i];
}
inline void add(int p, ll x) //单点修改
{
for(; p <= n; p += p & -p) save[p] += x;
}
inline ll sum(int l, int r) //区间求和
{
return sum(r) - sum(l - 1);
}
inline ll sum(int p)
{
ll res = 0;
for(; p; p -= p & -p) res += save[p];
return res;
}
};
class ExBinIdTree
{
int n;
BinIdTree bt_b, bt_c;
public:
explicit ExBinIdTree(const vector<ll> &src) : n(src.size() - 1) //由已知数组建立
{
vector<ll> b(n + 1), c(n + 1);
for(int i = 1; i <= n; i++)
{
b[i] = src[i] - src[i - 1];
c[i] = b[i] * (i - 1);
}
bt_b = BinIdTree(b), bt_c = BinIdTree(c);
}
inline void add(int l, int r, int x) //区间修改
{
bt_b.add(l, x);
bt_b.add(r + 1, -x);
bt_c.add(l, 1LL * (l - 1)*x);
bt_c.add(r + 1, -1LL * r * x);
}
inline ll sum(int l, int r) //区间求和
{
return sum(r) - sum(l - 1);
}
inline ll sum(int p)
{
return p * bt_b.sum(p) - bt_c.sum(p);
}
};
换成了
ll bit1[M],bit2[M];
inline void modify(int l, int r, int x)
{
ll x1 = 1ll*(l-1)*x, x2 = 1ll*r*x;
for(; l<=n; l+=l&-l)
bit1[l]+=x, bit2[l]+=x1;
for(++r; r<=n; r+=r&-r)
bit1[r]-=x, bit2[r]-=x2;
}
inline ll sum(int p)
{
ll res = 0, xp = p;
for(; p; p-=p&-p)
res += xp*bit1[p] - bit2[p];
return res;
}
inline ll sum(int l, int r)
{
return sum(r) - sum(l-1);
}
简洁,高效。
单调队列原理:如果一个元素又老又没用,那么就直接扔掉它。
如果完成同样功能的代码,不仅慢,而且难写,那为什么还要这样写?
之前的理由是为了封装,封装起来,以后调用就方便了。
但是我的封装,看起来并不能提高效率,不管是写代码的效率,还是运行的效率。而且还会影响代码的掌握程度,导致离了板子之后寸步难行,即使有板子也难以在灵活多变的编程中想到并应用相关的知识。
前几天做了一道题,这是学习他人代码之后的一段,功能是维护一个单调队列。
int q[M];
for(int i=1,l=0,r=0,t;i<=n;++i)
{
while(l<r && slope(q[l],q[l+1])<X[i]) l++; //pop_front
t = q[l];
dp[i] = C[i] + dp[t] - (sxp[i]-sxp[t]) + X[i]*(sp[i]-sp[t]);
while(l<r && slope(q[r-1],q[r])>slope(q[r],i)) r--; //pop_back
q[++r] = i; //push_back
}
这是我之前的单调队列板子
//Monoqueue.cpp 单调队列
class MonoQueue
{
deque<ll> dq;
queue<ll> que;
public:
void push(ll val)
{
que.push(val);
while(!dq.empty()&&dq.back()<=val)
dq.pop_back();
dq.push_back(val);
}
void pop()
{
if(!que.empty())
{
if(!dq.empty()&&dq.front()==que.front())
dq.pop_front();
que.pop();
}
}
ll getmax()
{
return dq.empty()?-1:dq.front();
}
ll getcnt()
{
return dq.size();
}
};
慢,难,丑,全占了。
一句话来说,如果需要强行魔改当前程序的目的去适应之前写的板子,那只能说明板子该换了。
我们不需要写过的代码,只需要从中学到的思想。
真正需要封装的,是那些具有属性和行为的实体,比如计算几何中的点、线、面,或者细节一成不变功能固定的数据结构(虽然我还没学几个)。
封装是为了写码便捷,逻辑清晰,而不是为了安抚自己的懒惰,需要找到这个平衡点。过分封装反而会导致逻辑混乱,写码迟钝,还有长期不接触细节导致的代码能力变弱。
使用STL也是如此。
用板子的目的与封装相差无几,板子多并不代表学会的东西多。我觉得使用板子的平衡点应当在于:如果这里不抄板子,那么就只好背代码了。
感觉自己最近的代码能力很弱,很多题能想到思路可是无法实现,第一个原因肯定是写码太少,还有一些别的原因,比如错误地使用了板子,因此记录如上。
附新版入口,将typedef改成了using,删去了不常用的快写函数,在考虑要不要删去save数组。
<snippet>
<content><![CDATA[
/* LittleFall : Hello! */
#include
using namespace std; using ll = long long; inline int read();
const int M = 500016, MOD = 1000000007;
int save[M];
int main(void)
{
#ifdef _LITTLEFALL_
freopen("in.txt","r",stdin);
#endif
${1:printf("Talk is cheap.");}
return 0;
}
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
]]></content>
<tabTrigger>sa</tabTrigger>
<scope>source.c++</scope>
</snippet>
}