【总结】代码能力(板子篇)

提前优化是万恶之源。

引子

什么叫做真正的代码能力?一直以来我都觉得自己的代码能力不错,直到我碰到了这道题:

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>

}

你可能感兴趣的:(总结)