【总结】算法相关

Posted on 2016年7月29日 02:15

毕业了以后没有像前几年的学长一样,回纪中呆一个星期讲讲课看看题,辅导一下初中的小朋友;相反,很轻松的,Symbol只是叫我回去讲一节课作为任务。那天晚上,讲到差不多结束的时候,内心真的很内疚,因为没有好好准备,现场也没有黑板,讲课的效率不太好,浪费了大家的时间。最后实在没什么东西好讲,就和大家分享了一下我是怎么学算法的,说了说我对学习的一些看法。但,就算我讲得再多,台下学弟学妹们接受的总是有限的;所以我决定把我的想法写下来,希望与大家共勉。

自学(?)

到了高中,老师已经很难帮你讲解各种各样的算法了。OI题目设计的方面实在是太多,一个信息学教练全面精通也不太现实。那么如果学习算法,我们只有两种出路:

  1. 自学:上网找资料,看学长给的资料,刷这种类型的题目。需求:资料查找能力;自学理解能力;英文水平。
  2. 教学:找学长/老师帮忙讲解。需求:讲师良好的讲课能力;听课能力。

在高中,我绝大部分的算法都是自学的。自学是一种很好提高自己各方面水平的方式,缺点是比较耗费时间,而且也不适用于所有人。我就说说自己在这方面的提高吧:

  1. 资料查找能力:互联网时代很重要的一个能力。从各种途径找资料,能在快速的时间筛选材料,并且整理找到的资料。
  2. 自学理解能力:自从我看了一个晚上的CLRS里面的FFT大彻大悟后,我就知道自己的感性理解能力上了一个层次。
  3. 英文水平:很多资料(算法的讲解/题目)都是英文,看多了那OI的专业名词就没什么问题了(除去专业名词其他的也不会很难)。

合适的算法

找清楚自己的水平学习哪些算法是没问题的?需要重点学习哪些热门的算法?我会列出基本的算法的。

学习的步骤

  1. 找一个合适的时间(最好是连续几天)。找一个安静的适合思考的环境。保持思维的清晰和内心的平静。
  2. 资料的选取。可以是网上/别人推荐的,也可以是自己选取的。
  3. 画草稿和做笔记。自学时很容易被自己的大脑欺骗过去,认为“啊,这简单,我都会了”,但事后往往都会对重要的地方模糊。做笔记的要点是:你能根据笔记回想起学习的内容,多动笔,写下的才是自己的。
  4. 请教。有时会遇到思维上的坎,怎么也想不到。可以请教学长或者网上的大神们。对,实在想不到要勇于突破这个瓶颈。
  5. 练习。练习讲义中的题目,典型题目,或者是之前做过的题目。

这样大概第一轮的学习就完成了。在脑中建立大概的模型就行了。

遗忘中巩固知识

一般过了半年的时间,不常接触的算法会到一个瓶颈期(脑海中会有模糊的思路,但是具体写就不知道了)

像我,回去上了一个学期的文化课,SA就不会写了。

归纳总结

...

【Battle】

Posted on 2015年6月22日 22:11

FFT

理解的前提是背诵。

只用记住就可以了

理解FFT的关键有几点:点值表达,插值运算;N次单位复数根;蝶形运算:

Code:BZOJ 3527: [Zjoi2014]力

而理解NFT的关键是N次单位复数根上的折半引理这一性质:

Code: NFT的简单实现

一般的NFT对模数P是有要求的,一般满足P=A*2^X+1的形式(如167772161, 998244353, 1004535809)。
但现在已经不止于这些质数,如P=10^9+7也逐渐出现在国外的比赛中。(所以已经不能通过模数来判断这道题应不应该使用FFT)
由于进行多项式乘法后每一项必定小于P*P*N,那么只要取几个可以使用NFT的模数分别计算,用CRT合并出结果即可。
那么上面的三个质数就可以应付大多数情况了。

Code : 合并的简单实现

几种题目类型

按时间分治计算FFT

再前面的文章有提及。

生成元

使用生成元可以把乘积化成加法,从而达到卷积的形式。 UOJ#86.BZOJ3992

循环卷积

如果两个N次多项式相乘,但次数界只设为N,使用FFT会有什么样的效果? 一个原本存在的项F[x](x>N),会被累加到F[x%N]里面。

【JUNE】

Posted on 2015年5月31日 20:02

2-D

问题存在隐藏的“2”可以转化成二维平面上的问题。

【Codeforces 547D】Mike and Fish

【Codeforces 536D】Tavas in Kansas

【Codeforces 538H】Summer Dichotomy

平面图,点定位

细节的讨论较多,模板题目

第一类,第二类斯特林数

待补。

【May】

Posted on 2015年5月19日 16:51

【Codeforces 438E】The Child and Binary Tree

递推式的优化处理(FFT,按照时间分治)

2013集训队互测的城市规划

优化递推式的计算(F已经计算):\( G_{n} = \sum_{i=1}^{n-1} G_{i} * F_{n-i} \)

直接计算G的值的时间复杂度是平方级别的。但经过观察,整个式子是卷积形式的,使用FFT(快速傅里叶变换)可以优化计算时间复杂度。

但瓶颈是:G的计算依赖于G。如果式子是\( G_{n} = \sum_{i=1}^{n-1} F_{i} * F_{n-i} \),则是一个很基础的问题了。在G里的元素尚未确定的情况下,是不能够直接套用FFT的。

解决问题的方式是按照时间分治。类似于CDQ分治,计算前一段(已经计算的)对后一段(正在计算的)的贡献。

实现的方法如下:

  1. 定义SOLVE(l,r)为计算 \( G_{l} \cdots G_{r} \)的值。
  2. 定义mid = (l+r)/2。递归处理SOLVE(l, mid), 处理完成后,这是l到mid的G值都计算完毕了。
  3. 下面计算(l,mid)对(mid+1,r)的贡献。即,计算\( T_{n} = \sum_{i=l}^{mid} G_{i} * F_{n-i} \),T为(l,mid)的每一项对(mid+1,r)的每一项的贡献,这一部分可以使用FFT进行优化(卷积形式)。
  4. 处理SOLVE(mid+1, r)。

每两个位置i,j(i<j),他们之间的影响被分到了每一层计算,累加则能得到结果,总时间复杂度为\( O(NLog^{2}N)\)。

Codeforces 438E : The Child and Binary Tree

优化递推式的计算(C已经计算):\( G_{n} = \sum_{i=0}^{n-1} \sum_{j=0}^{n-i} C_{i} * G_{j} * G_{n-i-j} \)

这道题的瓶颈是G的计算需要更加细节的处理。在一些时候需要两倍计算:因为(i,j)与(j,i)都会对n产生影响,而只会在i<j的时候计算,所以在区间不相交的时候需要计算两次。

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long LL;

const int N = 6e5 + 5;
int Mod = 998244353;

int n, m, l, lm, x[N];

int tr[20][N], wi[20][N];
int a[N], ci;
int b[N], c[N], d[N], tp[N];

int pow(int a, int b)
{
	LL r(1), s(a);
	for(; b; b>>=1, s=s*s%Mod) if(b&1) r=r*s%Mod;
	return r;
}

void maketh(int L, int lm, int *tr, int *wi)
{
	for(int i=0, p, j, k; i<l; tr[i++] = p)
	for(p=0, j=i, k=0; k<lm; k++, j>>=1) p = (p<<1) + (j&1);
	wi[0] = 1, wi[1] = pow(3, (Mod-1) / l);
	for(int i=2; i<=l; i++) wi[i] = LL(wi[i-1]) * wi[1] % Mod;
}

void makett(int L)
{
	l=L, ci = pow(l, Mod-2);
}

void dft(int *a, int sig, int *tr, int *wi)
{
	for(int i=0; i<l; i++) tp[tr[i]] = a[i];
	int bei = l, hal, lei;
	for(int i=2; i<=l; i+=i)
	{
		hal = i>>1, lei = sig>0?0:l, bei>>=1;
		for(int j=0; j<hal; j++, lei+=bei*sig)
		for(int k=j, w=wi[lei]; k<l; k+=i)
		{
			int u=tp[k], v=LL(tp[k+hal])*w%Mod;
			tp[k] = (u+v) % Mod;
			tp[k+hal] = (u-v+Mod) % Mod;
		}
	}
	for(int i=0; i<l; i++) a[i]=tp[i];
}

void solve(int l, int r)
{
	if(l==r) return;
	
	int m = (l+r)>>1, lg = r-l, L;
	
	solve(l, m);
	for(L = 1, lm = 0; L <= lg * 2; L += L, ++lm);
	makett(L);
	
	for(int i=0; i<L; i++) b[i] = c[i] = d[i] = 0;
	for(int i=l; i<=m; i++) b[i-l] = a[i];
	for(int i=0; i<=lg; i++) c[i] = x[i];
	
	// DETAIL
	if(lg<=l) for(int i=0; i<=lg; i++) d[i] = (a[i] + a[i]) % Mod;
	else for(int i=l; i<=m; i++) d[i-l] = a[i];
	
	dft(b, 1, tr[lm], wi[lm]), dft(c, 1, tr[lm], wi[lm]), dft(d, 1, tr[lm], wi[lm]);
	for(int i=0; i<L; i++) b[i] = LL(b[i]) * c[i] % Mod * d[i] % Mod;
	dft(b, -1, tr[lm], wi[lm]);
	for(int i=0; i<L; i++) b[i] = LL(b[i]) * ci % Mod;
	
	for(int i=m+1; i<=r; i++) (a[i] += b[i-l]) %= Mod;
	
	solve(m+1, r);
}

int main()
{
	scanf("%d%d", &n, &m), a[0] = 1;
	for(int i=1, p; i<=n; i++) scanf("%d", &p), x[p] = 1;
	
	for(l=1, lm=0; l <= m*3; l+=l, ++lm) maketh(l, lm, tr[lm], wi[lm]);
	maketh(l, lm, tr[lm], wi[lm]);
	
	solve(0, m);
	for(int i=1; i<=m; i++) printf("%d\n", int(a[i]));
	
	return 0;
}

【Codeforces 343E】Pumping Stations(Gomory-Hu Tree的定义与构建)

定义

WIKI:一个带权无向图G(V,E)的Gomory-Hu树,代表了图中所有s-t最小割。使用V-1次最小割可以构建出Gomory-Hu树。

简单的来说,树上的两点之间的路径最小值,就是在原图中两点的最小割。

构建

  1. 我们一开始将所有点的父亲指向1。
  2. 枚举每一个点i,计算它和父亲fi的最小割,并连边。
  3. 再枚举对于点j,若满足fj=fi并且j与i在最小割的一边,那么把fj=i

实现

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 205;
const int M = 1005;

int n, m, v[N], fa[N], f[N];

namespace netw
{
	const int P = N;
	const int E = M + M;
	const int inf = 1e9;
	
	int to[E], next[E], end[P], f[E], tms;
	
	void link(int x, int y, int z)
	{
		to[++tms]=y, next[tms]=end[x], end[x]=tms, f[tms]=z;
		to[++tms]=x, next[tms]=end[y], end[y]=tms, f[tms]=z;
	}
	
	int dat[P], pred[P], cur[P], cnt[P], dist[P];
	
	int sap(int S, int T)
	{
		for(int i=1; i<=n; i++) dist[i]=1, cnt[i]=0, cur[i]=end[i];
		dat[S]=inf, dist[T]=0, cnt[1]=n-1, cnt[0]=1;
	
		int x(S), p, q, ret(0);
		for(;;)
		{
			for(p=cur[x]; p; p=next[p]) if(f[p]>0 && dist[to[p]]+1==dist[x]) break;
			if(p)
			{
				dat[to[p]]=min(dat[x],f[p]), pred[to[p]]=p, cur[x]=p, x=to[p];
				if(x==T) for(ret+=dat[T]; x!=S; x=to[pred[x]^1]) f[pred[x]]-=dat[T], f[pred[x]^1]+=dat[T];
			}
			else
			{
				if(--cnt[dist[x]]==0) return ret;
				for(dist[x]=n+1, q=end[x]; q; q=next[q]) if(f[q]>0 && dist[to[q]]+1<dist[x])
				dist[x]=dist[to[q]]+1, cur[x]=q;
				++cnt[dist[x]];
				if(dist[S]>n) return ret;
				if(x^S) x=to[pred[x]^1];
			}
		}
	}
	
	void paint(int x, int *v)
	{
		if(v[x]) return; v[x]=1;
		for(int p=end[x]; p; p=next[p]) if(f[p]) paint(to[p], v);
	}
	
	void bfs(int x, int *v)
	{
		for(int i=1; i<=n; i++) v[i] = 0;
		paint(x, v);
		for(int i=2, j; i<=tms; i+=2) j = f[i] + f[i+1], f[i] = f[i+1] = j / 2;
	}
}

int to[N+N], next[N+N], end[N], d[N+N], fv[N+N], tms;
int per[N], cp;

void cgg(int x, int fa)
{
	for(int p=end[x]; p; p=next[p]) if(!fv[p] && to[p]!=fa)
	{
		if(!cp || d[p]<d[cp]) cp=p;
		cgg(to[p], x);
	}
}

void sp(int x)
{
	int u(1), tp;
	for(int p=end[x]; p && u; p=next[p]) if(!fv[p]) u=0;
	if(u) {per[++per[0]] = x; return;}
	
	cp=0, cgg(x, 0), fv[cp] = fv[cp^1] = 1;
	tp=cp, sp(to[tp]), sp(to[tp^1]);
}

int main()
{
	scanf("%d%d", &n, &m), netw :: tms = 1;
	for(int i=1, x, y, z; i<=m; i++)
	{
		scanf("%d%d%d", &x, &y, &z);
		netw :: link(x, y, z);
	}
	
	for(int i=1; i<=n; i++) fa[i] = 1;
	for(int i=2; i<=n; i++)
	{
		f[i] = netw :: sap(i, fa[i]), netw :: bfs(i, v), f[0] += f[i];
		for(int j=i+1; j<=n; j++) if(v[j] && fa[j] == fa[i]) fa[j] = i;
	}
	// Gomory-Hu Tree 的 中心构造过程
	tms = 1;
	for(int i=2; i<=n; i++)
	{
		to[++tms]=i, next[tms]=end[fa[i]], end[fa[i]]=tms, d[tms]=f[i];
		to[++tms]=fa[i], next[tms]=end[i], end[i]=tms, d[tms]=f[i];
	}

	sp(1);
	
	printf("%d\n", f[0]);
	for(int i=1; i<n; i++) printf("%d ", per[i]);
	printf("%d\n", per[n]);
	
	return 0;
}

应用

Codeforces 343E:找出一个排列,使得相邻两项的最小割之和最大

最小割之和就是树上的边权总和:找出在Gomory-Hu树上的最大边权的一条边,假如它连接的是u与v,那么排列中u与v必定是相邻的,不然就会浪费这条边权了。这样分析下来,每一条边权都会取到,而这是能够达到的上界。

下面分析如何得到一组合法的解。找出树上一条边权最小的边,把树分为两半,递归处理,把两边的答案合并就可以了。这样做是可以达到上界的,因为最大的边必定会留到最后,而最小的边权由于约束最松,可以最先处理。

UVA 11594 : All Pairs Maximum Flow

直接套用构建的模板就可以。

UVA 11603 : Its all about the Bandwidth /HDU 4700 : FLOW

给出N点之间的最小割,构建出图G满足其约束。

一个合法的G必能转化成一个合法的Gomory-Hu树,所以直接构建一棵树就好了。找出其中一条最短的边L,将树分成两半A与B:A与B之间最小割均为边权L则合法,递归处理。(如何分割A与B?固定其中一个点X在A,与X的最小割值大于L则只能在A,否则放置于B)为什么这个算法一定能得到正确解呢?

【POJ 3986】Math Teacher's Homework

一道比较有趣的数位DP

【POJ 1777】Vivian's Problem

梅森素数的应用

【GDOI2015】Good Luck 2 U.

Posted on 2015年5月15日 14:59

一本正经的总结

就像段考之前老师怎么强调也好,自己总结提醒也好,翻遍整本错题本也好,还是会有意料之外的错误那样,这次GDOI的出现的问题也在情理之中。
这和每个人状态、心态都有关系,在一定的程度上也有运气的成分吧。

DAY1

直接问题:第二题的函数里面没有打long long,丢掉的60分还是很冤的。

根本问题:以往的比赛会在最后的15min内把程序大大小小仔细看一看,因为时间太紧了,所以这次只是简单的检查了一下空间问题,没有检查数据类型。

比赛结束前半个小时还是要预留一些时间来检查的,不要对自己的程序太过自信了,即使是对拍过的。

至于其他的题目也没有太大的问题,做题的时候比平时更加仔细了,所以导致时间非常紧。

DAY2

直接问题:最后一题想错了一个关键地方,丢掉了80分。

根本问题:今天的三道题都打了对拍,多余的时间根本是没有,只剩下40min去做第四题。想的时候思路是没有问题的,但在实现的时候被样例给带跑了,在一个部分写错了。

这道题是区分的题目,并不是很复杂,拿到分就是拿到了,没有拿到就是没有。这种思路题思考完后,要考虑:我实现的时候有没有细节问题,有没有BUG呢?

DAY3

直接问题:第一题最后没有打出来。

根本问题:实际上没有一题会做。

如果在省选的时候你发现一题都没有正解的思路你会慌张吗。其实有一点啦。最后我推了1.5h的第一题还是没有调出来,看来这类型的题目是不对我的胃口罢了。

INALL

上一年的GDOI也是这个问题,每一道题目都没有很好的切入点,所以情绪不是很愉悦,有点自暴自弃地去打暴力。从其中可以看出两点问题:1平时接触的题目的广度太小,一些思路题没见过思路就被局限了(像D3T4这道最短路一样),2心理自我调节能力还没到达极致,自己的实力并没有展开,被自己的紧张给约束了。

仔细想想五届的GDOI,都没能让自己完全满意。初一三等,初二在初中NO7,初三全部NO15,高一NO12,高二NO5,发挥的始终没到那个水平啊,最后的一年还是有点遗憾的吧。

一派胡言的感想

从2011到2015,从北江到北江,最终回到了原点。

这条路上,有人中途加入,有人放弃退出,也有人从头走到了现在。

一齐踏上这条长征路,也算是战友吧,真的很开心一起并肩战斗过。

We've come a long way from where we began
Oh I'll tell you all about it when I see you again

【黑科技合集】Tech

Posted on 2015年4月15日 22:16

黑科技就是处理一些问题的特殊技巧。

【1】两条路径在树上的交【from CF500G】

int depmax(int x, int y){return dep[x] > dep[y] ? x : y;}
int pathsection(int a, int b, int c, int d, int &e, int &f)
{
	int u = lca(a, b), v = lca(c, d);
	if(dep[u] > dep[v]) swap(a, c), swap(b, d), swap(u, v);
	if(lca(u, v) != u) return 0;
	
	e = depmax(lca(a, c), lca(a, d));
	f = depmax(lca(b, c), lca(b, d));
	if(max(dep[e], dep[f]) < dep[v]) return 0;
	e = depmax(e, v), f = depmax(f, v);
	return 1;
}

【2】处理\( L\le Dx \le R \) (\(Mod\) \(M\))下的最小自然数解【from CF500G】

LL G(LL M, LL D, LL L, LL R) // L <= Dx <= R (Mod M)
{
	if((L-1)/gcd(D,M) >= R/gcd(D,M)) return -1;
	if(D+D>M) return G(M, M-D, M-R, M-L);
	if((L-1)/D < R/D) return (L+D-1)/D;
	LL t=G(D, (D-M%D)%D, L%D, R%D);
	return (L+M*t+D-1)/D;
}