[WC 2015复习](四)数学

都是比较简单SB的东西,求各位去WC的神犇勿喷。

1、高斯消元

(1)[BZOJ 1013][JSOI 2008]球形空间产生器sphere

http://www.lydsy.com/JudgeOnline/problem.php?id=1013

首先我们设在n维空间中,点a的坐标为(a1,a2...an),点b的坐标为(b1,b2...bn),球心坐标(x1,x2...xn),球的半径为r

(a1-x1)^2+(a2-x2)^2+...+(an-xn)^2=r^2 ...①
(b1-x1)^2+(b2-x2)^2+...+(bn-xn)^2=r^2 ...②
看上去这两个式子都是二次的,不能套用一次的高斯消元,但是当我们①-②后,神奇的一幕发生了:
(a1-b1)x1+(a2-b2)x2+...+(an-bn)xn=[(a1^2-b1^2)+(a2^2-b2^2)+...+(an^2-bn^2)]/2
由于此题有n个未知数,我们需要n个方程。
以第一个点的坐标和第2~n+1个点相配对构造n个方程就ok了。常数项就是上面式子等号右边的部分
然后高斯消元搞定

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <cmath>

#define MAXN 20

using namespace std;

double pos[MAXN],a[MAXN][MAXN],ans[MAXN]; //pos[]保存第一个点的坐标,a[][]高斯消元用,a[][n+1]保存的是常数项,ans[]保存的是最终的答案、球心坐标

void Gauss(int n) //求n元一次方程组的解
{
	for(int i=1;i<=n;i++) //消掉第i行之后所有行的第i列
	{
		int maxr=0; //找到第i行以下的第i列绝对值最大的行号maxr
		for(int j=i;j<=n;j++)
			if(fabs(a[j][i])>fabs(a[maxr][i]))
				maxr=j;
		for(int j=1;j<=n+1;j++) //交换第i行和第maxr行
			swap(a[i][j],a[maxr][j]);
		for(int j=i+1;j<=n;j++) //将第i行之后所有行j的第i列消去
		{
			double tmp=-a[j][i]/a[i][i];
			for(int k=i;k<=n+1;k++)
				a[j][k]+=tmp*a[i][k];
		}
	}
	for(int i=n;i>=1;i--) //从最后一行开始回代、更新答案
	{
		for(int j=n;j>i;j--) //第i行的第i+1~n列中的每一列j都要消去
			a[i][n+1]-=a[i][j]*ans[j];
		ans[i]=a[i][n+1]/a[i][i];
	}
}

int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++) //输入第一个点的坐标
		scanf("%lf",&pos[i]);
	for(int i=1;i<=n;i++) //输入第2~n+1号点的坐标
	{
		double tmp[MAXN]; //保存第i+1号点的坐标
		for(int j=1;j<=n;j++)
		{
			scanf("%lf",&tmp[j]);
			a[i][j]=pos[j]-tmp[j];
			a[i][n+1]+=pos[j]*pos[j]-tmp[j]*tmp[j];
		}
		a[i][n+1]/=2;
	}
	//高斯消元
	Gauss(n);
	//输出答案
	for(int i=1;i<=n;i++)
		printf("%.3lf%c",ans[i],i==n?'\n':' ');
	return 0;
}

2、线性代数

(1)[BZOJ 1297][SCOI 2009]迷路(矩阵快速幂)

http://www.lydsy.com/JudgeOnline/problem.php?id=1297

首先考虑naive的情况:任意两点之间的边权均为1,那么在n*n的矩阵map中,map[i][j]=1表示i到j有一条边,那么用T的时间从点1到点n的方案数就是自乘T次后的map的map[1][n]。

但是现在任意两点之间的边权在[1,9]范围内,那么我们可以把每个点i拆成9个点,用Node i,1~Node i,9来表示它们,Node i,1~Node i,9在矩阵map中依次连边,对于边权为w的边i->j而言,我们就在矩阵map中给Node i,w向Node j,1连边,这样在矩阵map中,要从Node i,1走到Node j,1就需要经过w条边权为1的边,最终的答案就是自乘T次后的map的map[1][n]。

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>

#define MAXN 110
#define MOD 2009

using namespace std;

struct Matrix
{
	int num[MAXN][MAXN];
	int n,m; //n*m大小矩阵
	Matrix()
	{
		n=m=0;
		memset(num,0,sizeof(num));
	}
}map,one;

Matrix operator*(Matrix a,Matrix b)
{
	Matrix c;
	c.n=a.n,c.m=b.m;
	for(int k=1;k<=a.m;k++)
		for(int i=1;i<=c.n;i++)
			for(int j=1;j<=c.m;j++)
				c.num[i][j]=(c.num[i][j]+((a.num[i][k]*b.num[k][j])%MOD))%MOD;
	return c;
}

Matrix fastPow(Matrix base,int pow)
{
	Matrix ans=one;
	while(pow)
	{
		if(pow&1) ans=ans*base;
		base=base*base;
		pow>>=1;
	}
	return ans;
}

int num(int x,int t) //点x的第t个时间节点
{
	return (x-1)*9+t;
}

int main()
{
	int n,t;
	char s[MAXN];
	scanf("%d%d",&n,&t);
	for(int i=1;i<=9*n;i++)
        one.num[i][i]=1;
    map.n=map.m=one.n=one.m=n*9;
	for(int i=1;i<=n;i++) //每个点的9个状态链接起来
		for(int j=1;j<=8;j++)
			map.num[num(i,j)][num(i,j+1)]=1;
	for(int i=1;i<=n;i++)
	{
		scanf("%s",s+1);
		for(int j=1;j<=n;j++)
			if(s[j]!='0')
				map.num[num(i,s[j]-'0')][num(j,1)]=1;
	}
	map=fastPow(map,t);
	printf("%d\n",map.num[num(1,1)][num(n,1)]);
	return 0;
}

3、容斥原理

(1)[BZOJ 1042][HAOI 2008]硬币购物

http://www.lydsy.com/JudgeOnline/problem.php?id=1042

下面约定c[i]=第i种硬币的面值,d[i]=第i种硬币的个数。

比较显然的思路就是用凑出面值sum的总方案数(不限制硬币个数的方案数)-凑出面值sum的不合法方案数(限制硬币个数的条件下,有些种类的硬币个数超出了限制的方案数)。

用DP求出总方案数,用f[i]表示不限制硬币个数的前提下,凑出面值i的方案数。f[i]=(f[j-c[k]]),1≤k≤4,为了避免重复计算方案数,可以以k为阶段,分4次统计最终的答案。

那么最终的答案=f[sum]-1号硬币超额的方案数-2号硬币超额的方案数-3号硬币超额的方案数-4号硬币超额的方案数+1号和2号都超额的方案数+...+3号和4号都超额的方案数-1号、2号、3号都超额的方案数-...-...+1、2、3、4号都超额的方案数

那么怎么求第i种硬币超额的方案数呢?不妨设当前已经用第i种硬币付了(d[i]+1)*c[i]元钱,那么还剩sum-(d[i]+1)*c[i]元钱没付,这部分剩余的钱超不超额就无所谓了,反正第i种硬币已经超额了,所以方案数为f[sum-(d[i]+1)*c[i]]

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>

#define MAXN 100010

using namespace std;

typedef long long int LL;

LL ans,f[MAXN]; //f[i]=不考虑限制条件,得到面值i的方案数
int c[5],d[5];

void DFS(int x,int k,int sum) //在用第x种硬币,k为偶数加,k为奇数就减(也可以说k就是超额的硬币种类数),sum=还没付清的面值
{
	if(sum<0) return;
	if(x==5)
	{
		if(k&1) ans-=f[sum];
		else ans+=f[sum];
		return;
	}
	DFS(x+1,k+1,sum-(d[x]+1)*c[x]); //Case 1:第x种硬币超额了,此次多付了(d[x]+1)*c[x]元
	DFS(x+1,k,sum); //Case 2:第x种硬币没超额
}

int main()
{
	int T;
	for(int i=1;i<=4;i++) scanf("%d",&c[i]);
	scanf("%d",&T);
	f[0]=1;
	for(int i=1;i<=4;i++) //DP预处理
		for(int j=c[i];j<MAXN;j++)
			f[j]+=f[j-c[i]];
	while(T--)
	{
		for(int i=1;i<=4;i++) scanf("%d",&d[i]); //输入每种硬币提供的个数
		int x; //要买x元商品
		ans=0;
		scanf("%d",&x);
		DFS(1,0,x);
		printf("%lld\n",ans);
	}
	return 0;
}

4、数学期望

(1)[BZOJ 3566][SHOI/HBOI 2014]概率充电器(树上期望)

http://www.lydsy.com/JudgeOnline/problem.php?id=3566

首先我们要知道期望的一个性质:对于两个相互独立的事件A、B,P(A+B)=P(A)+P(B)-P(AB)。

而一个点被通上电有三种情况:1、它自己带电。2、它的儿子给它传上了点。3、它的父亲给它传上了电。

那么我们可以分两次DFS。

第一次DFS只统计一个点i自己带电和被它的儿子通上电的概率pNode[i]。很好计算,pNode[i]=pNode[i]+pNode[son]*p[edge]-pNode[i]*pNode[son]*p[edge]

第二次DFS要在第一次DFS得到每个点导电概率的基础上,再统计上一个点i被它的父亲通上电的概率,最终得到每个点i导电的概率f[i]。

第二次做得要麻烦一些,因为会出现重复计算的问题,第一次DFS时求了儿子对父亲的贡献,但是这次是父亲对儿子的贡献,对于每对父亲和儿子来说,不能把父亲对儿子的贡献算进儿子对父亲的贡献。

不妨设对于点u而言,它除了儿子v给它的贡献以外的导电概率为y,可以得出

pNode[v]*p[edge]+y-y*pNode[v]*p[edge]=f[u]

(1-pNode[v]*p[edge])y=f[u]-pNode[v]*p[edge]

得到y=(f[u]-pNode[v]*p[edge])/(1-pNode[v]*p[edge])

这样我们就可以得到,儿子v从父亲u获得的贡献就是y(也就是说y是除去了v对u的贡献的u的导电概率),因此f[v]=pNode[v]+y*p[edge]-pNode[v]*y*p[edge]

最终答案就是所有点的导电概率之和,即f[i]

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <cmath>
 
#define MAXN 1001000
#define EPS (1e-6)
 
using namespace std;
 
struct edge
{
    int u,v,next;
    double p;
}edges[MAXN];
 
int head[MAXN],nCount=0;
 
void AddEdge(int U,int V,double P)
{
    edges[++nCount].u=U;
    edges[nCount].v=V;
    edges[nCount].p=P;
    edges[nCount].next=head[U];
    head[U]=nCount;
}
 
double pNode[MAXN]; //pNode[i]=点i在子树中被冲上电的概率(不算父结点给它导电)
double f[MAXN]; //f[i]=点i充上电的概率
double ans=0;
 
bool dcmp(double a) //true表明a==0,false表明a!=0
{
    return fabs(a-0)<EPS;
}
 
void DFS1(int u,int fa) //第一次DFS,累计上u的儿子给u传电增加的期望,从点u开始DFS,u的父亲是fa
{
    for(int p=head[u];p!=-1;p=edges[p].next)
    {
        int v=edges[p].v;
        if(v==fa) continue;
        DFS1(v,u);
        pNode[u]=pNode[u]+pNode[v]*edges[p].p-pNode[u]*edges[p].p*pNode[v];
    }
}
 
void DFS2(int u,int fa) //从点u开始DFS,u的父亲是fa
{
    ans+=f[u];
    for(int p=head[u];p!=-1;p=edges[p].next)
    {
        int v=edges[p].v;
        if(v==fa) continue;
        double tmp=(1-pNode[v]*edges[p].p);
        if(dcmp(tmp)) f[v]=1.0; //特判,pNode[v]*edges[p].p==1的情况
        else
        {
            double y=(f[u]-pNode[v]*edges[p].p)/(1.0-pNode[v]*edges[p].p);
            f[v]=pNode[v]+y*edges[p].p-pNode[v]*y*edges[p].p;
        }
        DFS2(v,u);
    }
}
 
int main()
{
    memset(head,-1,sizeof(head));
    int n;
    scanf("%d",&n);
    for(int i=1;i<n;i++)
    {
        int u,v,p;
        scanf("%d%d%d",&u,&v,&p);
        AddEdge(u,v,(double)p/100.0);
        AddEdge(v,u,(double)p/100.0);
    }
    for(int i=1;i<=n;i++)
    {
        scanf("%lf",&pNode[i]);
        pNode[i]/=100.0;
    }
    DFS1(1,-1);
    f[1]=pNode[1]; //根节点1只能被儿子传上电
    DFS2(1,-1);
    printf("%lf\n",ans);
    return 0;
}

5、欧拉函数

(1)[BZOJ 2705][SDOI 2012]Longge的问题

http://www.lydsy.com/JudgeOnline/problem.php?id=2705

我们可以把问题由Σgcd(i,N)的过程中枚举i变为枚举gcd(i,N)的值,然后加权累计答案。这样枚举次数会少很多。假设gcd(i,N)=k,则gcd(i/k,N/k)=1,因此Σgcd(i,N)可以转化为Σk*gcd(i/k,N/k) , gcd(i/k,N/k)=1,也就是i/k与N/k互质。而1<=i<=N,因此i/k<=N/k,对于给定的k而言,gcd(i/k,N/k)的个数就是在[1,N/k]范围内与N/k互质的数的个数,问题转化为了求欧拉函数,也就是gcd(i/k,N/k)=fai(N/k)。故Σgcd(i,N)=Σfai(N/k)*k,k是N的约数。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <cmath>
 
using namespace std;
 
typedef long long int LL;
 
LL sqrtn;
 
LL h(LL x) //欧拉函数h(x)
{
    LL ans=x;
    for(LL i=2;i<=sqrtn;i++)
    {
        if(x%i==0)
        {
            ans=ans/i*(i-1);
            while(x%i==0) x/=i;
        }
    }
    if(x>1) ans=ans/x*(x-1);
    return ans;
}
 
int main()
{
    LL n,ans=0;
    scanf("%lld",&n);
    sqrtn=sqrt(n);
    for(LL i=1;i<=sqrtn;i++) //枚举n的约数i
    {
        if(n%i==0) //i是n的约数
        {
            ans+=i*h(n/i);
            if(i*i<n) ans+=(n/i)*h(i);
        }
    }
    printf("%lld\n",ans);
    return 0;
}

6、组合数学

(1)[BZOJ 3505][CQOI 2014]数三角形

http://www.lydsy.com/JudgeOnline/problem.php?id=3505

很不错的一道组合数学题。

不妨设整个网格中是n点*m点大小的,

首先我们要得到3个点放入n点*m点,总共n*m点中的方案数=C(n*m,3)

然后减去3个点在同一行的方案数=n*C(m,3)

再减去3个点在同一列的方案数=m*C(n,3)

还要减去3个点斜向共线的情况,这个情况特殊一些,

我们可以枚举斜向共线的3个点中最外面两个点之间的坐标差,假设x轴坐标差为i,y轴坐标差为j,那么假如已经确定了最外面两个点的坐标,则中间的那个点放置的方案数=gcd(i,j)-1,这个可以画图找规律发现。然后假如已知i、j,最外面两个点放置的方案数为2*(n-i)*(m-j)  (注:同一方向最外面两个点放置的方案数为(n-i)*(m-j),而最外面两个点可以按照左上-右下对角线方向放置,也可以按照左下-右上对角线方向放置,因此要乘个2),枚举每个(i,j),减去对应的不合法方案数即可。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>

#define MAXN 1005010

using namespace std;

typedef long long int LL;

LL C[MAXN][5];
int n,m;

void GetCombination() //打表求组合数C[][]
{
	C[0][0]=1;
	for(int i=1;i<=n*m;i++)
		for(int j=0;j<=4;j++)
		{
			if(i==j||j==0) C[i][j]=1;
			else C[i][j]=C[i-1][j]+C[i-1][j-1];
		}
}

int gcd(int a,int b)
{
	if(b==0) return a;
	return gcd(b,a%b);
}

int main()
{
	LL ans=0;
	scanf("%d%d",&n,&m);
	n++,m++; //n、m变成了点的个数
	GetCombination();
	ans+=C[n*m][3]; //答案初始化为n*m个点里任选3个点的方案数
	ans-=n*C[m][3]; //去掉行上共线的方案数
	ans-=m*C[n][3]; //去掉列上共线的方案数
	//最后去掉斜线上共线的方案数
	for(int i=1;i<n;i++) //枚举斜线上共线的最外面两点之间的x轴坐标差i、y轴坐标差j
		for(int j=1;j<m;j++)
		{
			int tmp=gcd(i,j)-1; //tmp=中间的点的位置个数
			if(tmp>=1) //中间的点有位置放
				ans-=tmp*2*(n-i)*(m-j); //ans-=(固定了外边两点的位置,中间的点放置的方案数)*(已知外边两点坐标差i、j,外边两点在n*m个点中放置的方案数)
		}
	printf("%lld\n",ans);
	return 0;
}

7、莫比乌斯函数、莫比乌斯反演

(1)[BZOJ 2440][中山市选2011]完全平方数

http://www.lydsy.com/JudgeOnline/problem.php?id=2440

题目大意:求区间[1,+)中第k小的不是完全平方数倍数的数。

整体思路:容斥原理、莫比乌斯函数。

不妨设集合S包含了所有不是完全平方数的数。观察到随着区间[1,n]的右端点数字的增大,集合S中的数字个数也会增多,也就是说答案具备单调性,因此很自然的想法便是二分答案。

于是问题转化为了给定n,求[1,n]中有多少数字在集合S内。我们可以用容斥原理求出这个问题的反问题的答案(求[1,n]中有多少数字不在集合S内)。

这里约定一个概念:平方质数,就是是一个质数的二次方的数。

最终答案=n-( n/4+n/9+n/25+……{平方数因数是一个平方质数})+( n/36+n/100+……{含两个平方质数的因数})-{含三个平方质数的因数} ……

但是这样做很麻烦,不妨考虑用莫比乌斯函数来解决此题。


而我们知道在区间[1,n]中是x倍数的数字个数有个,不妨用i2来表示一个完全平方数,则我们可以枚举i,答案是:

[WC 2015复习](四)数学_第1张图片

公式的解释:很显然i不会超过sqrt(n),因为有条件i2≤n,另外实际上这个公式简化过了,题目说1不是完全平方数,那么i=1的情况实际上并不能算进去,答案实际上应该是

而i=1时,,故可以把上式简化。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>

#define MAXN 51000
#define INF 2000000000

using namespace std;

typedef long long int LL;

int miu[MAXN];

void getMiu() //打表求莫比乌斯函数
{
	for(LL i=2;i<MAXN;i++) //初始时miu[i]=i表示i是质数,否则为i最大的素因子
	{
		if(!miu[i]) //i是质数
		{
			miu[i]=i;
			for(LL j=i*i;j<MAXN;j+=i) //所有的j都是质数i的倍数
				miu[j]=i;
		}
	}
	//之后miu[i]就是莫比乌斯函数,初始化μ(1)=1
	miu[1]=1;
	for(LL i=2;i<MAXN;i++)
	{
		if(i%(miu[i]*miu[i])==0) //数字i有大于1的平方素因子
			miu[i]=0;
		else //否则数字i无大于1的平方素因子,不妨设p为i最大的质因数,miu[i]=-miu[i/p]
			miu[i]=-miu[i/miu[i]];
	}
}

LL cal(LL n) //求区间[1,n]中有多少个不是完全平方数
{
	LL ans=0;
	for(LL i=1;i*i<=n;i++)
		if(miu[i])
			ans+=miu[i]*n/i/i;
	return ans;
}

int main()
{
	int TestCase;
	getMiu();
	scanf("%d",&TestCase);
	while(TestCase--)
	{
		LL n;
		scanf("%lld",&n);
		LL ans,lowerBound=1,upperBound=2*n+1;
		while(lowerBound<=upperBound)
		{
			LL mid=(lowerBound+upperBound)>>1;
			if(cal(mid)>=n)
			{
				ans=mid;
				upperBound=mid-1;
			}
			else lowerBound=mid+1;
		}
		printf("%lld\n",ans);
	}
	return 0;
}


(2)[BZOJ 2440][HAOI 2011]Problem b

http://www.lydsy.com/JudgeOnline/problem.php?id=2440

比较显然的是gcd(i,j)=k等价于gcd(i/k,j/k)=1。不妨设solve(n,m)表示求i属于[1,n],j属于[1,m]的互质的有序的(i,j)对数。考虑到最终要求的是有序的gcd(i,j)=k的(i,j)对数,因此最终的答案很显然为solve(b/k,d/k)-solve((a-1)/k,d/k)-solve(b/k,(c-1)/k)+solve((a-1)/k,(c-1)/k)。

那么考虑solve(n,m)的过程,不妨设n<m,显然我们可以枚举一个i,得到所有的有序对(x,y),其中xi的倍数,y也是i的倍数,那么这样的x的个数为n/iy的个数为m/i。有序对(x,y)的个数为(n/i)*(m/i)。

一个很朴素的写法如下:

LL _G(int a,int b)          //朴素算法
{
     int tx=min(a,b),ty=max(a,b);
     LL ans = 0;
     for(int i = 1; i <= tx; i++)
         ans += (LL)mu[i]*(tx/i)*(ty/i);
     return ans;
}
但是遗憾的是,由于题目范围很大,这种朴素做法会TLE。
注意到这里的(tx/i)和(ty/i)在连续的一段i中是相同的,那么我们可以将这一部分连续的i合并成一个块,这一块的miu[i]之和乘上(tx/i)*(ty/i)即可。

这时候发现虽然经过了分块,但是由于有对miu[i]求和的过程,问题的复杂度仍然没有降低。

但是显然这里的miu[i]之和是可以用前缀和优化,于是成功降维!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
 
#define MAXN 100050
 
using namespace std;
 
typedef long long int LL;
 
int miu[MAXN];
 
void getMiu()
{
    memset(miu,0,sizeof(miu));
    for(LL i=2;i<MAXN;i++)
    {
        if(!miu[i])
        {
            miu[i]=i;
            for(LL j=i*i;j<MAXN;j+=i)
                miu[j]=i;
        }
    }
    //求莫比乌斯函数
    miu[1]=1;
    for(LL i=2;i<MAXN;i++)
    {
        if(i%(miu[i]*miu[i])==0)
            miu[i]=0;
        else
            miu[i]=-miu[i/miu[i]];
    }
}
 
int sum[MAXN];
 
LL solve(int n,int m) //分块优化求i属于[1,n],j属于[1,m]互质的(i,j)对数
{
    LL ans=0;
    if(n>m) swap(n,m);
    for(int i=1,p=0;i<=n;i=p+1) //p=分块的编号
    {
        p=min(n/(n/i),m/(m/i));
        ans+=(LL)(sum[p]-sum[i-1])*(n/i)*(m/i);
    }
    return ans;
}
 
int main()
{
    getMiu(); //求莫比乌斯函数
    for(int i=1;i<MAXN;i++)
        sum[i]=sum[i-1]+miu[i];
    int a,b,c,d,k;
    int T;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d%d%d%d",&a,&b,&c,&d,&k);
        LL ans=solve(b/k,d/k)-solve((a-1)/k,d/k)-solve(b/k,(c-1)/k)+solve((a-1)/k,(c-1)/k);
        printf("%lld\n",ans);
    }
    return 0;
}










你可能感兴趣的:([WC 2015复习](四)数学)