《算法竞赛进阶》学习笔记

N数码判定性问题

大意就是给你两个N数码(N为奇数),判定是否能互相转化的问题
首先展开成一维,空格不计

1 2 3
4 5 6
7 0 8转化为1 2 3 4 5 6 7 8
然后判断两个序列的逆序对数奇偶性是否相同,相同则有解
证明:
在空格左右移动的时候,展开序列不改变
在空格上下移动的时候,展开序列中有一个数会前移(后移)n-1位,由于n为奇数则n-1为偶数。于是改变的逆序对个数也为偶数个(可以自己yy一下)
得证

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long LL;
int n,a[265000],b[265000];
int s[265000];
int lowbit(int x){return x&-x;}
void change(int x){while(x<=n*n-1)s[x]++,x+=lowbit(x);}
int findsum(int x){int ret=0;while(x){ret+=s[x],x-=lowbit(x);}return ret;}
int main()
{
//	freopen("a.in","r",stdin);
	while(scanf("%d",&n)!=EOF)
	{
		int lx=0,ly=0;
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
			{
				int x;scanf("%d",&x);
				if(x!=0)a[++lx]=x;
			}
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
			{
				int x;scanf("%d",&x);
				if(x!=0)b[++ly]=x;
			}
		memset(s,0,sizeof(s));
		LL sx=0,sy=0;
		for(int i=lx;i>=1;i--){sx+=(LL)findsum(a[i]);change(a[i]);}
		memset(s,0,sizeof(s));
		for(int i=ly;i>=1;i--){sy+=(LL)findsum(b[i]);change(b[i]);}
		if((sx%2)==(sy%2))printf("TAK\n");
		else printf("NIE\n");
	}
	return 0;
}

倍增替代二分

其实就是在对于一种答案很小但二分上界极大的二分的替代,此时可以将常数省为log(答案)
可以设一个倍增长度p,初始=1
每次往后扩展p的长度,如果可以扩展则扩展且p*=2
如果不能则p/=2直到为0为止
此时为最大
有一道题Genius ACM
倍增+归并的题,放一个板子

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long LL;
LL k;int n,m,a[510000];
LL b[510000],c[510000],d[510000],G[510000];
inline LL sqr(LL x){return x*x;}
void megsort(int l,int r)
{
	if(l>=r)return ;
	int mid=(l+r)/2;
	megsort(l,mid);megsort(mid+1,r);
	int t=0,u=l,v=mid+1;
	while(u<=mid && v<=r)
	{
		if(c[u]<=c[v])d[++t]=c[u++];
		else d[++t]=c[v++];
	}
	while(u<=mid)d[++t]=c[u++];
	while(v<=r)d[++t]=c[v++];
	for(int i=1;i<=t;i++)c[i+l-1]=d[i];
}
bool check(int S,int st,int ed)
{
	int len=0,lx=(st-1)-S+1;
	for(int i=st;i<=ed;i++)c[++len]=a[i];
	megsort(1,len);
	int t=0,u=1,v=1;
	while(u<=lx && v<=len)
	{
		if(b[u]<c[v])d[++t]=b[u++];
		else d[++t]=c[v++];
	}
	while(u<=lx)d[++t]=b[u++];
	while(v<=len)d[++t]=c[v++];
	for(int i=1;i<=t;i++)G[i]=d[i];
	len=ed-S+1;
	LL sum=0;u=min(m,len/2);
	for(int i=1;i<=u;i++)sum+=sqr(G[i]-G[len-i+1]);
	if(sum<=k)
	{
		for(int i=1;i<=t;i++)b[i]=G[i];
		return true;
	}
	return false;
}
int main()
{
//	freopen("a.in","r",stdin);
	int TT;scanf("%d",&TT);
	while(TT--)
	{
		scanf("%d%d%lld",&n,&m,&k);
		for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
		int ans=0,s=1,t=1,p=1;b[1]=a[1];
		while(t<n)
		{
			if(p==0){ans++;s=++t;b[1]=a[s];p=1;continue;}
			if(check(s,t+1,min(t+p,n)))
			{
				t+=p,p*=2;
				if(t>n){ans++;break;}
			}
			else p/=2;
		}
		if(t==n)ans++;
		printf("%d\n",ans);
	}
	return 0;
}

最小表示法

给出一个字符串S,将其首尾连接。求出在这个环中以一个字符切开后形成的链字典序最小,求出这个字符
朴素算法就把每种切分方式求出来排序,复杂度 O ( n 2 l o g n ) O(n^2logn) O(n2logn)
一种 O ( n ) O(n) O(n)的算法思路是:
先将字符串倍增一倍接在末尾
定义两个指针i,j,初始i=1,j=2,k=0
枚举k直到ch[i+k]!=ch[j+k]
如果ch[i+k] < ch[j+k],则j~j+k这一段都不可能成为最小表示。
因为任选其一,在ii+k中选出对应的字符拉出的链一定会比在jj+k中的字符拉出的链表示法小
大于的情况同理
只需要跳过这一段,对于第一种情况时j=j+k+1,如果i==j那么j++
另一种情况相反即可
枚举到i或者j长度大于len时跳出,这时候i,j的min值即为最小表示法的字符位置

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
using namespace std;
char ch[210000];
int main()
{
	scanf("%s",ch+1);int len=strlen(ch+1);
	for(int i=len+1;i<=2*len;i++)ch[i]=ch[i-len];
	int i=1,j=2,k;
	while(i<=len && j<=len)
	{
		for(k=0;ch[i+k]==ch[j+k] && k<=len;k++);
		if(k==len)break;
		if(ch[i+k]-'0'<ch[j+k]-'0')
		{
			j=j+k+1;
			if(j==i)j++;
		}
		else
		{
			i=i+k+1;
			if(i==j)i++;
		}
	}
	printf("%d\n",min(i,j));
	return 0;
}

哈夫曼树

一棵k叉哈夫曼树的定义有:
每个节点的贡献为该节点的权值*该节点到根的路径长度,要求贡献最小
对于k叉哈夫曼树的建法,首先在数组末尾补0直至 ( n − 1 ) m o d ( k − 1 ) = 0 (n-1)mod(k-1)=0 (n1)mod(k1)=0,其中n为数组长度。然后将其扔进小根堆中,每次取出堆顶的k个元素求和累计答案,再将k个元素的和放入小根堆中。如此重复直到堆中仅剩1个元素
代码给出bzoj4198 荷马史诗

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<queue>
using namespace std;
typedef long long LL;
struct node
{
	int meg;LL c;
	node(){meg=0;c=0;}
	friend bool operator <(node n1,node n2)
	{
		if(n1.c!=n2.c)return n1.c>n2.c;
		return n1.meg>n2.meg;
	}
};
priority_queue<node> q;
int n,k;
LL w[210000];
node cnt[15];
int main()
{
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++)scanf("%lld",&w[i]);
	while((n-1)%(k-1))w[++n]=0;
	for(int i=1;i<=n;i++)
	{
		node tmp;tmp.c=w[i];tmp.meg=0;
		q.push(tmp);
	}
	LL ans=0;
	while(q.size()!=1)
	{
		for(int i=1;i<=k;i++)cnt[i]=q.top(),q.pop();
		node tmp;tmp.meg=0;tmp.c=0;
		for(int i=1;i<=k;i++)tmp.c+=cnt[i].c,tmp.meg=max(tmp.meg,cnt[i].meg);
		tmp.meg++;ans+=tmp.c;
		q.push(tmp);
	}
	node tmp=q.top();
	printf("%lld\n%d\n",ans,tmp.meg);
	return 0;
}

线段树扫描线

从左往右扫,离散y值
矩形左边赋值1,右边赋值-1,每扫到一条边的时候在线段树里处理一下这条边的覆盖情况。左边就加上,右边就减掉
处理之前计算答案
板子poj1151

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
using namespace std;
int n,tim;
//LS
double w[1100];int tlen;
struct edge{double l,r,x;int k;}a[1100];//上坐标 下坐标 x坐标 
bool cmp(edge n1,edge n2){return n1.x<n2.x;}
int fd(double num)
{
	int l=1,r=tlen,ret;
	while(l<=r)
	{
		int mid=(l+r)/2;
		if(w[mid]<num)l=mid+1;
		else r=mid-1,ret=mid;
	}
	return ret;
}
//seg
int cover[1100];
double sum[1100];
void pushup(int now,int l,int r)
{
	if(cover[now])sum[now]=w[r]-w[l];
	else if(l+1==r)sum[now]=0;
	else sum[now]=sum[now<<1]+sum[now<<1|1];
}
void change(int now,int l,int r,int ql,int qr,int c)
{
	if(l==ql && r==qr)
	{
		cover[now]+=c;
		pushup(now,l,r);
		return ;
	}
	int mid=(ql+qr)/2;
	if(r<=mid)change(now<<1,l,r,ql,mid,c);
	else if(mid<=l)change(now<<1|1,l,r,mid,qr,c);
	else 
	{
		change(now<<1,l,mid,ql,mid,c);
		change(now<<1|1,mid,r,mid,qr,c);
	}
	pushup(now,ql,qr);
}
int main()
{
	while(scanf("%d",&n)!=EOF)
	{
		tim++;tlen=0;
		memset(cover,0,sizeof(cover));
		memset(sum,0,sizeof(sum));
		if(!n)break;
		for(int i=1;i<=n;i++)
		{
			double x1,y1,x2,y2;
			scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
			a[2*i-1].l=a[2*i].l=y1;a[2*i-1].r=a[2*i].r=y2;
			a[2*i-1].x=x1;a[2*i].x=x2;
			a[2*i-1].k=1;a[2*i].k=-1;
			w[++tlen]=y1;w[++tlen]=y2;
		}
		sort(a+1,a+1+2*n,cmp);
		sort(w+1,w+1+tlen);
		tlen=unique(w+1,w+1+tlen)-(w+1);
		n*=2;
		double ans=0;
		a[0].x=a[1].x;
		for(int i=1;i<=n;i++)
		{
			ans+=(a[i].x-a[i-1].x)*sum[1];
			double upper=fd(a[i].l),downer=fd(a[i].r);
			change(1,upper,downer,1,tlen,a[i].k);
		}
		printf("Test case #%d\n",tim);
		printf("Total explored area: %.2lf \n\n",ans);
	}
	return 0;
}

环形dp

从前只会把环拆掉然后复制一段乱搞
lyd告诉我还有一种方法
先考虑问题在非环下的情况,然后再考虑成环后与非环相比增加的情况,把这些情况暴力加上跑两次dp比较最优解
##有后效性的dp
如果列出dp方程发现转移方向形成了一个环
可以考虑使用高斯消元直接解出答案

多重背包

在将物品拆分成二进制的情况下复杂度是 O ( n m log ⁡ c [ i ] ) O(nm\log c[i]) O(nmlogc[i])的,其中 c [ i ] c[i] c[i]表示第i种物品的数量
考虑使用单调队列优化
在进行枚举的时候,我们会发现 j − v [ i ] j-v[i] jv[i] j − 1 − v [ i ] j-1-v[i] j1v[i]这两个状态不可能会互相转移
于是对余数分组,余数分别为 0 , 1 , 2 , . . . , v [ i ] − 1 0,1,2,...,v[i]-1 0,1,2,...,v[i]1
分别对余数 j , j + v [ i ] , j + 2 ∗ v [ i ] . . . j,j+v[i],j+2*v[i]... j,j+v[i],j+2v[i]...进行dp
推柿子再使用单调队列优化可以做到 ( n m ) (nm) (nm)

单调队列与斜率优化

使用单调队列的时候,我们往往可以写出一个如下类型的转移方程
f [ i ] = m i n ( f [ j ] + v a l ( j , i ) ) f[i]=min(f[j]+val(j,i)) f[i]=min(f[j]+val(j,i))
其中 v a l ( j , i ) val(j,i) val(j,i)是一个与i,j相关的多项式方程
能使用单调队列的情况当且仅当 v a l ( i , j ) val(i,j) val(i,j)中每一项只与i或者j相关
因为我们每次都是将与i相关的放在一起,与j相关的放在一起。i不变的时候j决策单调,i变化j不变的时候决策仍然单调

斜率优化其实相当于进化版的单调队列
此时 v a l ( i , j ) val(i,j) val(i,j)中有一项或几项与i和j的乘积相关
我们可以同样将只与i,j相关的项置于一侧,将于乘积相关的置于一侧,最后将其化简为
Y ( i ) − Y ( j ) X ( i ) − X ( j ) \frac {Y(i)-Y(j)}{X(i)-X(j)} X(i)X(j)Y(i)Y(j)
的斜率形式,其中X,Y函数分别表示了一些常量相加
维护单调下凸壳或上凸壳即可

四边形不等式

定义:若在坐标系中有二元函数 w ( x , y ) w(x,y) w(x,y),有任意 a < b < = c < d a<b<=c<d a<b<=c<d
若函数都满足 w ( a , c ) + w ( b , d ) < = w ( a , d ) + w ( b , c ) w(a,c)+w(b,d)<=w(a,d)+w(b,c) w(a,c)+w(b,d)<=w(a,d)+w(b,c),则称该函数满足四边形不等式
另一种定义方式:若任意 a < b a<b a<b
若函数都满足 w ( a + 1 , b + 1 ) + w ( a , b ) < = w ( a , b + 1 ) + w ( a + 1 , b ) w(a+1,b+1)+w(a,b)<=w(a,b+1)+w(a+1,b) w(a+1,b+1)+w(a,b)<=w(a,b+1)+w(a+1,b),该函数也满足四边形不等式

简称:相交不大于包含

一维线性dp的四边形不等式优化
对于转移方程 f [ i ] = m i n ( f [ j ] + v a l ( j , i ) ) f[i]=min(f[j]+val(j,i)) f[i]=min(f[j]+val(j,i)),设 p [ i ] p[i] p[i]表i取到最优解时候的决策点。若p满足在1~n上单调不减,则称f具有决策单调性
定理:若函数 v a l ( j , i ) val(j,i) val(j,i)满足四边形不等式,则f数组满足决策单调性,p数组单调不降
证明:
对于任意 i > j i>j i>j j < = p [ i ] − 1 j<=p[i]-1 j<=p[i]1,有
f [ p [ i ] ] + v a l ( p [ i ] , i ) < = f [ j ] + v a l ( j , i ) f[p[i]]+val(p[i],i)<=f[j]+val(j,i) f[p[i]]+val(p[i],i)<=f[j]+val(j,i)
对于任意 k > i k>i k>i,因为 v a l ( j , i ) val(j,i) val(j,i)满足四边形不等式,所以有
v a l ( j , i ) + v a l ( p [ i ] , k ) < = v a l ( j , k ) + v a l ( p [ i ] , i ) val(j,i)+val(p[i],k)<=val(j,k)+val(p[i],i) val(j,i)+val(p[i],k)<=val(j,k)+val(p[i],i)
移项有
v a l ( p [ i ] , k ) − v a l ( p [ i ] , i ) < = v a l ( j , k ) − v a l ( j , i ) val(p[i],k)-val(p[i],i)<=val(j,k)-val(j,i) val(p[i],k)val(p[i],i)<=val(j,k)val(j,i)
1式与3式相加有
f [ p [ i ] ] + v a l ( p [ i ] , k ) < = f [ j ] + v a l ( j , k ) f[p[i]]+val(p[i],k)<=f[j]+val(j,k) f[p[i]]+val(p[i],k)<=f[j]+val(j,k)
此时f[k]在p[i]取值仍会比在j处取值优,换言之f[k]的取值不会取到0~p[i]-1处
故p数组满足单调不减
f具有决策单调性

搜索的奇技淫巧

1:有一个叫做bitset的东西可以 / 32 /32 /32的常数,转化二进制的时候多想想这个
2:直接 2 n 2^n 2n判定性问题搜过不去的时候,不妨试一试折半搜索。前一半正常搜出答案并记录排序去重,后一半搜出答案后二分与第一段搜出的答案合并求解,复杂度大约可以降为 2 n 2 + 2 n 2 ∗ log ⁡ n 2 2^{\frac{n}{2}}+2^{\frac{n}{2}}*\log{\frac{n}{2}} 22n+22nlog2n
3:从大到小或者从小到大排序后与当前答案比较大小搜索剪枝会有奇效谁试谁知道
4:对于拼接性问题可以选择排除冗杂状态,比如限制加入的数是从大到小的,因为先加x再加y和先加y再加x的搜索分支是相同的
5:毫无头绪的时候不妨推推柿子
6:在能确定答案较小的时候,可以通过限制搜索的深度来减少不必要的较深的搜索过程
7:如果不能使BFS序列值单调不降的时候,试试合并状态或者多套一个BFS?

线性空间与高斯消元

给出一个方程组,要求求解这个方程组,此时可以考虑高斯消元
在保证有解的情况下可以解出一个阶梯型矩阵
无解的情况下则会存在两种元
主元和自由元,主元的定义为在第j列,仅有第i行的数不为0,在第i行,第1~j-1列的数均为零。
自由元:枚举到一个变量,如果剩下的行中这个变量的系数都为0,这个变量就是自由元
高斯消元可以解普通,异或方程组

向量有两种运算
1:向量加法:a+b其中a,b均为向量
2:标量乘法:k*a其中k为常数,a为向量
给定一些向量a1,a2,a3…ak,通过如上两种运算可以得到的向量显然能组成一个空间,定义这个空间为线性空间。这些向量称为这个线性空间的生成子集
当选出线性空间内一些向量a1,a2,a3,…,ak时,其中任意一个向量都无法被其他向量表示出时,我们称这些向量线性无关,否则线性相关
线性无关的生成子集我们称为这个线性空间的基底,简称基。一个线性空间的所有基所包含的向量数目都是相等的,这个数我们称为这个线性空间的维数

对于一个nm的矩阵,这个矩阵的任意一个格子都存在一个数
把矩阵的每一行看成一个大小为m的行向量,这m个行向量可以构成一个线性空间,这个线性空间的维数称为这个矩阵的秩
把这个n
m的矩阵看作一个系数矩阵进行高斯消元,消元后不全为0的行向量线性无关,因为高斯消元的原理就是向量加法和标量乘法。所以这些行向量可以被称为上面这个线性空间的基
例题:bzoj4004

卡特兰数

给定n个0和n个1,将其进行排列,要求任意前缀中0的个数都不少于1的个数,求这样的数列的个数
这就是卡特兰数了,相当于一个火车进站问题
定义式: f [ n ] = C 2 n n n + 1 f[n]=\frac{C_{2n}^n}{n+1} f[n]=n+1C2nn
推论:
n辆火车进出栈问题的答案等于卡特兰数的第n项
n个左括号和n个右括号组成的合法括号序列的数量为卡特兰数的n项
n个节点构成的不同二叉树数量为卡特兰数的第n项
平面直角坐标系从(1,1)走到(n,m),只能向右或者向上且不接触直线y=x的方案数是卡特兰数的第n-1项的两倍

容斥原理

这里写图片描述
证明:
设一个数a共在k个集合中出现过
选一个集合的时候这个数会被选中 C k 1 C_k^1 Ck1
两个集合时这个数会被选中 C k 2 C_k^2 Ck2
三个集合时这个数会被选中 C k 3 C_k^3 Ck3
k个集合时这个数会被选中 C k k C_k^k Ckk
根据容斥原理公式有
T = C k 1 − C k 2 + C k 3 − . . . + ( − 1 ) k + 1 C k k T=C_k^1-C_k^2+C_k^3-...+(-1)^{k+1}C_k^k T=Ck1Ck2+Ck3...+(1)k+1Ckk
根据二项式定理有
( 1 − x ) k = C k 0 − C k 1 x + C k 2 x 2 − . . . + ( − 1 ) k C k k x k (1-x)^k=C_k^0-C_k^1x+C_k^2x^2-...+(-1)^kC_k^kx^k (1x)k=Ck0Ck1x+Ck2x2...+(1)kCkkxk
可以写为
( 1 − x ) k = C k 0 − ( C k 1 x − C k 2 x 2 + . . . − ( − 1 ) k C k k x k ) (1-x)^k=C_k^0-(C_k^1x-C_k^2x^2+...-(-1)^kC_k^kx^k) (1x)k=Ck0(Ck1xCk2x2+...(1)kCkkxk)
x取1,发现后面的式子等于T
又发现左边项为0, C k 0 C_k^0 Ck0=1
所以T=1
所以每个数仅会且仅会被选中1次

博弈

基础的NIM取石子游戏
结论:当每堆石子数a1 ^ a2 ^ a3 an>0时,先手必胜,否则先手必败
证明:当每堆石子均为0时,必定为必败态不做考虑
当石子数异或和不为0时,一定能从某一堆石子中取出相对应的一些石子使得异或和为0。设当前异或和为x,我们从某一堆中取出一些石子使其成为a[i]^x颗,此时异或和为0
当石子数异或和等于0时,无论做什么操作都不能使石子数异或和再为0
得证

有向图游戏
有一个有向无环图,初始棋子在起点,轮流移动棋子,不能移动的人失败
有向图中的一个点x的SG值= m e x ( S G ( y ) ) ( x − > y ) mex(SG(y))(x->y) mex(SG(y))(x>y)
当一个点能到达必败态的时候,这个点的SG值一定不为0
当一个点不能到达任何必败态的时候,这个点的SG值为0
整个有向图游戏G的SG值定义为起点的SG值,即 S G ( G ) = S G ( s t ) SG(G)=SG(st) SG(G)=SG(st)

有向图游戏的和
给定m个有向图游戏,定义一种游戏p为:每次选定一个有向图游戏对上面的棋子进行操作,不能操作者失败。
这个游戏p的SG值为各游戏的SG值的异或和
S G ( p ) = S G ( G 1 ) x o r S G ( G 2 ) x o r . . . x o r S G ( G m ) SG(p)=SG(G_1) xorSG(G_2)xor...xorSG(G_m) SG(p)=SG(G1)xorSG(G2)xor...xorSG(Gm)

upd:辣鸡忘了阶梯博弈
把相邻两个点看作一组
石子数相当于这两个点的距离
如果是奇数个就第一个与0的相距
做nim就可以了

数学小姿势

1:一个合数m中一定含有至少一个小于 m \sqrt m m 的质因子

2:N的所有正约数和可以表示为
设N表示成为 p 1 c 1 ∗ p 2 c 2 ∗ . . . ∗ p n c n p_1^{c1}*p_2^{c2}*...*p_n^{cn} p1c1p2c2...pncn
于是约数和可表示为
( 1 + p 1 + p 1 2 + . . . + p 1 c 1 ) ∗ ( 1 + p 2 + p 2 2 + . . . + p 2 c 2 ) ∗ . . . ∗ ( 1 + p n + p n 2 + . . . + p n c n ) (1+p_1+p_1^2+...+p_1^{c1})*(1+p_2+p_2^2+...+p_2^{c2})*...*(1+p_n+p_n^2+...+p_n^{cn}) (1+p1+p12+...+p1c1)(1+p2+p22+...+p2c2)...(1+pn+pn2+...+pncn)
用乘法分配率证明即可,因为每个因子均可以表示为
p 1 g 1 ∗ p 2 g 2 ∗ . . . ∗ p n g n p_1^{g1}*p_2^{g2}*...*p_n^{gn} p1g1p2g2...pngn,其中g1<=c1,g2<=c2,以此类推

3:1~N的每个数约数和大约为 N log ⁡ n N\log n Nlogn

4:对于任意a,b<=n
g c d ( a , b ) = g c d ( a , a − b ) gcd(a,b)=gcd(a,a-b) gcd(a,b)=gcd(a,ab) g c d ( 2 a , 2 b ) = 2 g c d ( a , b ) gcd(2a,2b)=2gcd(a,b) gcd(2a,2b)=2gcd(a,b)
第二个显然成立,第一个的证明如下
g c d ( a , b ) = d gcd(a,b)=d gcd(a,b)=d,则a,b均可以表示为dk1,dk2,a-b即可以表示为d*(k1-k2),此情况下一式显然成立

5:对于单求一个数n的欧拉函数有如下公式
n = p 1 c 1 ∗ p 2 c 2 ∗ . . . ∗ p n c n n=p_1^{c1}*p_2^{c2}*...*p_n^{cn} n=p1c1p2c2...pncn
ϕ n = n ∗ ( p 1 − 1 ) / p 1 ∗ ( p 2 − 1 ) / p 2 ∗ . . . ∗ ( p n − 1 ) / p n \phi n=n*(p1-1)/p1*(p2-1)/p2*...*(pn-1)/pn ϕn=n(p11)/p1(p21)/p2...(pn1)/pn
= n ∗ ( 1 − 1 / p 1 ) ∗ ( 1 − 1 / p 2 ) ∗ . . . ∗ ( 1 − 1 / p n ) =n*(1-1/p1)*(1-1/p2)*...*(1-1/pn) =n(11/p1)(11/p2)...(11/pn)
证明:
设p为n的质因子,则n中含有p这个因子的数共有 n / p n/p n/p
设q为n的质因子,则n中含有q这个因子的数共有 n / q n/q n/q
这些数一共有 n / p + n / q n/p+n/q n/p+n/q个(算上重复)
这其中p*q的倍数被除去了2次,我们要加上 n / ( p ∗ q ) n/(p*q) n/(pq)
答案为 n − n / p − n / q + n / ( p ∗ q ) n-n/p-n/q+n/(p*q) nn/pn/q+n/(pq)
提取n可得 n ∗ ( 1 − 1 / p − 1 / q + 1 / ( p ∗ q ) ) = n ∗ ( 1 / p ) ∗ ( 1 / q ) n*(1-1/p-1/q+1/(p*q))=n*(1/p)*(1/q) n(11/p1/q+1/(pq))=n(1/p)(1/q)
如此类推即可

6:对于任意正整数n,在1~n中与之互质的数的和为 n ∗ ϕ n / 2 n*\phi n/2 nϕn/2
证明:由于 g c d ( n , x ) = g c d ( n , n − x ) gcd(n,x)=gcd(n,n-x) gcd(n,x)=gcd(n,nx),所以任意一个与n互质的数x必定有一个数n-x与之对应。可以知道这样的数的平均值为 n / 2 n/2 n/2。又由于这些数只有 ϕ n \phi n ϕn个,故值为 n ∗ ϕ n / 2 n*\phi n /2 nϕn/2

7:若a,b互质,则 ϕ ( a b ) = ϕ ( a ) ∗ ϕ ( b ) \phi (ab)=\phi(a)*\phi(b) ϕ(ab)=ϕ(a)ϕ(b)
证明:对ab分解质因数,又因为a,b互质,所以a和b的质因数中不可能包含有对方的质因数,则ab的质因数集合为a,b的质因数集合的并集。此时根据欧拉函数定义式可得如上式

8:费马小定理:若p是质数,则对于任意整数a有:
a p ≡ a ( m o d    p ) a^p\equiv a(\mod p) apa(modp) a p − 1 ≡ 1 ( m o d    p ) a^{p-1}\equiv 1(\mod p) ap11(modp)

9:欧拉定理:若正整数a,n互质,则有
a ϕ n ≡ 1 ( m o d    n ) a^{\phi n}\equiv 1(\mod n) aϕn1(modn)
推论:
若正整数a,n互质,则对于任意整数b有
a b ≡ a b m o d    ϕ n a^b\equiv a^{b \mod \phi n} ababmodϕn

10:组合数性质
C n m = n ! m ! ∗ ( n − m ) C_n^m= \frac{n!}{m!*(n-m)} Cnm=m!(nm)n!
C n m = C n n − m C_n^m=C_n^{n-m} Cnm=Cnnm
n个中取出m个的集合,剩下一定有n-m个的集合与之一一对应
C n m = C n − 1 m + C n − 1 m − 1 C_n^m=C_{n-1}^m+C_{n-1}^{m-1} Cnm=Cn1m+Cn1m1
有两种取法:取n与不取n
取n要在前n-1个中取m-1即为 C n − 1 m − 1 C_{n-1}^{m-1} Cn1m1
不取n是在前n-1个中取m即为 C n − 1 m C_{n-1}^m Cn1m
加法原理即可
C 0 + C 1 + C 2 + . . . + C n = 2 n C^0+C^1+C^2+...+C^n=2^n C0+C1+C2+...+Cn=2n
n个元素中任取组成集合,有n+1种方法即为取0,1,2,…,n个,分别为 C 0 , C 1 . . . C n C^0,C^1...C^n C0,C1...Cn
其中每个元素要不取要不不去,两者相同,即为 2 n 2^n 2n个选择方法

11:二项式定理
( a + b ) n = ∑ k = 0 n C n k a k b n − k (a+b)^n=\sum_{k=0}^nC_n^ka^kb^{n-k} (a+b)n=k=0nCnkakbnk
其实就是杨辉三角瞎画个图就知道的…

12:错排
先给个递推吧
D [ n ] D[n] D[n]表示n个元素错排的方案数
D [ n ] = ( n − 1 ) ∗ ( D [ n − 1 ] + D [ n − 2 ] ) D[n]=(n-1)*(D[n-1]+D[n-2]) D[n]=(n1)(D[n1]+D[n2])

Floyd求最小环

首先依据floyd的定义有当外层循环至k时,mp[i][j]保存了经过编号不超过k-1的节点的最短路径
对于每次枚举到k的时候,先不进行floyd操作
枚举两个点i,j,可以找出一个环mp[i][j]+a[i][k]+a[k][j],其中a数组保存了原有的边
我们枚举了一个k节点和与之相邻的两个节点构成的环,易知这个环的组成方式是由k,i,j于一些不超过k-1的节点组成
满足答案
模板poj1734

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#define mem(x,y) memset(x,y,sizeof(x))
using namespace std;
struct nd{int pa[105],ln,c;}mp[105][105];
int a[105][105],n,m;
int path[105],len;
void getpa(int i,int j,int k)
{
	len=1;path[1]=i;
	for(int u=1;u<=mp[i][j].ln;u++)path[++len]=mp[i][j].pa[u];
	path[++len]=k;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)mp[i][j].c=2e9;
	mem(a,63);
	for(int i=1;i<=m;i++)
	{
		int x,y,c;scanf("%d%d%d",&x,&y,&c);
		mp[x][y].c=mp[y][x].c=min(mp[x][y].c,c);
		mp[x][y].ln=1;mp[x][y].pa[1]=y;
		mp[y][x].ln=1;mp[y][x].pa[1]=x;
		a[x][y]=a[y][x]=min(a[x][y],c);
	}
	int ans=999999999;
	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<k;i++)
			for(int j=i+1;j<k;j++)
				if(a[j][k]<1e9 && a[k][i]<1e9 && mp[i][j].c<1e9)
					if(ans>mp[i][j].c+a[j][k]+a[k][i])
					{
						ans=mp[i][j].c+a[j][k]+a[k][i];
						getpa(i,j,k);
					}
		for(int i=1;i<=n;i++)if(i!=k)
			for(int j=1;j<=n;j++)if(j!=k && j!=i)
				if(mp[i][k].c<1e9 && mp[k][j].c<1e9)
					if(mp[i][j].c>mp[i][k].c+mp[k][j].c)
					{
						for(int u=1;u<=mp[i][k].ln;u++)mp[i][j].pa[u]=mp[i][k].pa[u];
						for(int u=1;u<=mp[k][j].ln;u++)mp[i][j].pa[u+mp[i][k].ln]=mp[k][j].pa[u];
						mp[i][j].ln=mp[i][k].ln+mp[k][j].ln;
						mp[i][j].c=mp[i][k].c+mp[k][j].c;
					}
	}
	if(ans==999999999)printf("No solution.\n");
	else
	{
	//	printf("%d\n",ans);
		for(int i=1;i<len;i++)printf("%d ",path[i]);
		printf("%d\n",path[len]);
	}
	return 0;
}

最小度限制生成树

此处仅讨论当根节点有限制时的生成树
除去根节点后,我们发现原有的生成树被分成了几个连通块
预处理这些连通块的MST
再分别在这些连通块的MST中找到一个节点与1相连,使这条边总是这个连通块中与1相连的边最小的
设共有T个连通块,有S的度数限制
那么仍有S-T条边可以与1相连供我们修正
枚举边u,设这条边连的点为(1,x),权为c。若这条边不在当前的MST中,我们暴力找出x到1路径上边权最大的边,设其权为cost。若c < cost则代表可以更换。我们找到能使得cost-c最大的边u,删去在u的基础上边权最大的边再连起来。如此操作S-T次或cost-u恒不大于0为止
模板poj1639

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<string>
#include<map>
using namespace std;
struct node{int x,y,c,next,other;bool v;}a[21000];int len,last[1100];
void ins(int x,int y,int c){len++;a[len].x=x;a[len].y=y;a[len].c=c;a[len].v=true;a[len].next=last[x];last[x]=len;}
struct edge{int x,y,c;bool v;}e[11000];
bool cmp(edge n1,edge n2){return n1.c<n2.c;}
map<string,int> q;int cnt;
char ch[105];
int fa[1100];
int findfa(int x){return fa[x]==x?fa[x]:fa[x]=findfa(fa[x]);}
int n,belong[1100],S;
int maxline,maxcal;
bool fd(int pos,int fa)
{
	if(pos==1)return true;
	for(int k=last[pos];k;k=a[k].next)
		if(a[k].v&&fa!=a[k].y)
		{
			int y=a[k].y;
			if(fd(y,pos))
			{
				if(maxcal<a[k].c)maxline=k,maxcal=a[k].c;
				return true;
			}
		}
	return false;
}
int pushup()
{
	int u=-1,gg=0,po=0;
	for(int i=1;i<=n;i++)
		if(e[i].x==1&&e[i].v)
		{
			int x=e[i].x,y=e[i].y,cost=e[i].c;
			maxcal=0;fd(e[i].y,0);
			if(cost>maxcal)continue;
			else if(maxcal-cost>po){po=maxcal-cost;gg=maxline,u=i;}
		}
	if(u==-1)return -1;
//	printf("update:%d %d %d\n",e[u].x,e[u].y,e[u].c);
	int x=e[u].x,y=e[u].y,cost=e[u].c;e[u].v=false;
	maxline=gg;
	a[maxline].v=a[a[maxline].other].v=false;
//	printf("checknow:%d %d %d\n",a[maxline].x,a[maxline].y,a[maxline].c);
	ins(e[u].x,e[u].y,e[u].c);ins(e[u].y,e[u].x,e[u].c);
	a[len].other=len-1;a[len-1].other=len;
	return po;
}
int main()
{
	q.clear();
	ch[1]='P';ch[2]='a';ch[3]='r';ch[4]='k';q[ch+1]=++cnt;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",ch+1);
		if(q[ch+1]==0)q[ch+1]=++cnt;
		e[i].x=q[ch+1];
		scanf("%s",ch+1);
		if(q[ch+1]==0)q[ch+1]=++cnt;
		e[i].y=q[ch+1];
		if(e[i].x>e[i].y)swap(e[i].x,e[i].y);
		scanf("%d",&e[i].c);e[i].v=true;
	}
	sort(e+1,e+1+n,cmp);
	for(int i=1;i<=cnt;i++)fa[i]=i;
	int ans=0;
	for(int i=1;i<=n;i++)
		if(e[i].x!=1&&e[i].y!=1)
		{
			int u=findfa(e[i].x),v=findfa(e[i].y);
			if(u!=v)
			{
				fa[u]=v;
				ins(e[i].x,e[i].y,e[i].c);ins(e[i].y,e[i].x,e[i].c);
				a[len].other=len-1;a[len-1].other=len;
				ans+=e[i].c;e[i].v=false;
			}
		}
	scanf("%d",&S);
	int block=0;
	for(int i=2;i<=cnt;i++)
		if(fa[i]==i)belong[i]=++block;
	for(int i=2;i<=cnt;i++)belong[i]=belong[findfa(i)];
	for(int i=1;i<=block;i++)
	{
		int u=0;
		for(int j=1;j<=n;j++)
			if(e[j].x==1 && belong[e[j].y]==i){u=j;break;}
		ins(e[u].x,e[u].y,e[u].c);
		ins(e[u].y,e[u].x,e[u].c);ans+=e[u].c;e[u].v=false;
		a[len].other=len-1;a[len-1].other=len;
	}
//	for(int i=1;i<=n;i++)printf("CHKER:%d %d %d %d\n",e[i].x,e[i].y,e[i].c,e[i].v);
	if(block==S){printf("Total miles driven: %d\n",ans);return 0;}
	for(int i=block+1;i<=S;i++)
	{
		int tmp=pushup();
		if(tmp<=0)break;
		ans-=tmp;
	}
	printf("Total miles driven: %d\n",ans);
	return 0;
}

割点与割边

割点:删去这个点后图变为至少两个不连通的连通块,则这个点是割点
判定:tarjan的时候,对于存在low[y]>=dfn[x]的情况,x即为割点。在x=1(为搜索树的根)的情况下需特判至少两个点y满足low[y]>=dfn[x]才为割点

割边:删去这条边后图变为至少两个不连通的连通块,则这条边是割边
判定:tarjan的时候,枚举每条边,如存在low[y]>dfn[x](注意没有等于)的情况,则该边为割边
注意重边的情况,所以我们tarjan的时候传父亲节点到当前节点的边的编号进入

##无向图点双联通
放个求的模板

void tarjan(int x)
{
	dfn[x]=low[x]=++id;sta[++tp]=x;
	if(x==root && last[x]==0)
	{
		dcc[++cnt].push_back(x);
		return ;
	}
	int flag=0;
	for(int k=last[x];k;k=a[k].next)
	{
		int y=a[k].y;
		if(dfn[y]==-1)
		{
			tarjan(y);
			low[x]=min(low[x],low[y]);
			if(low[y]>=dfn[x])
			{
				flag++;
				if(x!=root || flag>1)cut[x]=1;
				int i;cnt++;
				do
				{
					i=sta[tp--];
					dcc[cnt].push_back(i);
				}while(i!=y);
				dcc[cnt].push_back(x);
			}
		}
		else low[x]=min(low[x],dfn[y]);
	}
}

这里是缩点的板子

int gg=cnt;
for(int i=1;i<=n;i++)if(cut[i])belong[i]=++gg;
for(int i=1;i<=cnt;i++)
{
	for(int j=0;j<dcc[i].size();j++)
	{
		int x=dcc[i][j];
		if(cut[x])
		{
			ins(belong[x],i),ins(i,belong[x]);
		}
		cal[x]=i;
}

欧拉路

判定欧拉图:一个无向图为欧拉图,当且仅当这个图连通且每个点的度数均为偶数
判定欧拉路:一张无向图中存在欧拉路,当且仅当这个图连通,仅有两个节点度数为奇数其他均为偶数,则这两个点是欧拉路的起点和终点

2-sat的输出方案

发现tarjan完会缩成一个DAG图,且这个DAG图是对称的
对于每一个SCC,如果其没有出边代表选了他是不会有后继影响

一种稍复杂的输出方案:
设opp[belong[i]]=belong[n+i],opp[belong[n+i]]=belong[i]
其中belong[i]表示i点所在的SCC编号
在缩完点后的图上建反图,跑拓扑序。初始val[i]=-1
每次从队头取出一个入度为0的点i(相当于在原图中出度为0),如果他的val=-1,则将val[i]赋为0,val[opp[i]]赋为1
这里赋为0的SCC代表取了,赋为1的代表没取
最后枚举1~n的所有点,如果val[belong[i]]=0代表这个点只能取0,反之取1

较简单的输出方案:
发现tarjan的SCC编号已经是自底向上的拓扑序编号
我们可以直接比较两个SCC的编号大小来确定是否选择

for(int i=1;i<=n;i++)opp[belong[i]]=belong[i+n],opp[belong[i+n]]=belong[i];
for(int i=1;i<=2*n;i++)val[i]=belong[i]>opp[belong[i]];

如果belong[i]>opp[belong[i]]则代表i所在的SCC编号比opp[belong[i]]的编号大,拓扑序中会先选到opp[belong[i]],所以赋值为1,反之为0

二分图的一些性质

团 :任意两点之间都有一条边相连的子图被称为无向图的团

1:二分图的最小点覆盖=最大匹配

2:二分图的最大独立集=总点数-最小点覆盖=总点数-最大匹配

3:无向图G的最大团=其补图G’的最大独立集

4:给定一张n个点的DAG图G,要求其最小不可重路径覆盖(即用尽量少的不相交路径,覆盖DAG图的每个顶点刚好一次)的做法:将图中每个点i拆点i和i+n。1~n的点置于二分图左侧,n+1~2*n的点置于右侧。原图中的边(x,y)改为(x,y+n)。对这个二分图求最大匹配,那么DAG图G的最小路径覆盖即为n-二分图最大匹配

5:给定一张n个点的DAG图G,要求其最小可重路径覆盖(即每个点可以被覆盖多次)。先对这张图进行传递闭包,即假设(x,y)间接连通我们直接添上一条有向边x->y。然后对这张新图做最小可重路径覆盖即可

6:二分图G的必须边与可行边
必须边:在该图G的任意最大匹配中都存在边u,则u是必须边
可行边:在该图G的某一最大匹配中存在边u,则u是可行边
显而易见可行边包含了必须边
考虑如何判断这些边,此处直接介绍更一般的情况(最大匹配不是完备匹配)
使用最大流求出任一个最大匹配,在残余网络上跑tarjan(判断条件多一个剩余流量是否有,有才进入tarjan)
必须边:边(x,y)的节点x,y不在一个连通块中且剩余流量为0
可行边:边(x,y)的节点x,y在一个连通块中或者剩余流量为0

证明:我们发现残余网络上正向边一定有一部分剩余流量为0,其反向弧流量为1。这充量于从y->x有一条有向边,代表x->y这条边是匹配边。在正向边剩余流量为1的边上,充量于x->y有一条有向边,代表x->y这条边是非匹配边。如果x,y在同一个连通块中,则相当于x->y这条边被选为了匹配边,同时还有另外一条路径能使得x到达y。我们断掉x->y这条边并将另外那条路径匹配边选择情况取反,不会改变最大匹配数
证毕

简单最大平面图

若图G(n,m)是极大简单平面图,则有m<=3n-6

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值