母函数标本
第一种:
有1克、2克、3克、4克的砝码各一 枚,能称出哪几种重量?每种重量各有几种可能方案?
考虑用母函数来解决这个问题:
我们假设x表示砝码,x的指数表示砝码的重量,这样:
1个1克的砝码可以用函数1+x表示,
1个2克的砝码可以用函数1+x2表示,
1个3克的砝码可以用函数1+x3表示,
1个4克的砝码可以用函数1+x4表示,
上面这四个式子懂吗?
我们拿1+x2来说,前面已经说过,x表示砝码,x的指数表示重量,即这里就是一个质量为2的砝码,那么前面的1表示什么?1代表重量为2的砝码数量为0个。(理解!)
不知道大家理解没,我们这里结合前面那句话:
“把组合问题的加法法则和幂级数的t的乘幂的相加对应起来“
1+x2表示了两种情况:1表示质量为2的砝码取0个的情况,x2表示质量为2的砝码取1个的情况。
这里说下各项系数的意义:
在x前面的系数a表示相应质量的砝码取a个,而1就表示相应砝码取0个,这里可不能简单的认为相应砝码取0个就该是0*x2(想下为何?结合数学式子)。
Tanky Woo 的程序人生:http://www.wutianqi.com/
所以,前面说的那句话的意义大家可以理解了吧?
几种砝码的组合可以称重的情况,可以用以上几个函数的乘积表示:
(1+x)(1+x2)(1+x3)(1+x4)
=(1+x+x2+x3)(1+x3+x4+x7)
=1+x+x2+2x3+2x4+2x5+2x6+2x7+x8+x9+x10
从上面的函数知道:可称出从1克到10克,系数便是方案数。(!!!经典!!!)
例如右端有2x5 项,即称出5克的方案有2:5=3+2=4+1;同样,6=1+2+3=4+2;10=1+2+3+4。
故称出6克的方案有2,称出10克的方案有1 。
接着上面,接下来是第二种情况:
求用1分、2分、3分的邮票贴出不同数值的方案数:
大家把这种情况和第一种比较有何区别?第一种每种是一个,而这里每种是无限的。
以展开后的x4为例,其系数为4,即4拆分成1、2、3之和的拆分数为4;
即 :4=1+1+1+1=1+1+2=1+3=2+2
这里再引出两个概念整数拆分和拆分数:
所谓整数拆分即把整数分解成若干整数的和(相当于把n个无区别的球放到n个无标志的盒子,盒子允许空,也允许放多于一个球)。
整数拆分成若干整数的和,办法不一,不同拆分法的总数叫做拆分数。
现在以上面的第二种情况每种种类个数无限为例,给出模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#include <iostream> using namespace std; // Author: Tanky Woo // www.wutianqi.com const int _max = 10001; // c1是保存各项质量砝码可以组合的数目 // c2是中间量,保存没一次的情况 int c1[_max], c2[_max]; int main() {//int n,i,j,k; int nNum; // int i, j, k; while(cin >> nNum) { for(i=0; i<=nNum; ++i) // ---- ① { c1[i] = 1; c2[i] = 0; } for(i=2; i<=nNum; ++i) // ----- ② { for(j=0; j<=nNum; ++j) // ----- ③ for(k=0; k+j<=nNum; k+=i) // ---- ④ { c2[j+k] += c1[j]; } for(j=0; j<=nNum; ++j) // ---- ⑤ { c1[j] = c2[j]; c2[j] = 0; } } cout << c1[nNum] << endl; } return 0; } |
我们来解释下上面标志的各个地方:(***********!!!重点!!!***********)
① 、首先对c1初始化,由第一个表达式(1+x+x2+..xn)初始化,把质量从0到n的所有砝码都初始化为1.
② 、 i从2到n遍历,这里i就是指第i个表达式,上面给出的第二种母函数关系式里,每一个括号括起来的就是一个表达式。
③、j 从0到n遍历,这里j就是(前面i個表達式累乘的表達式)里第j个变量,(這裡感謝一下seagg朋友給我指出的錯誤,大家可以看下留言處的討論)。如(1+x)(1+x^2)(1+x^3),j先指示的是1和x的系数,i=2执行完之后变为
(1+x+x^2+x^3)(1+x^3),这时候j应该指示的是合并后的第一个括号的四个变量的系数。.
④ 、 k表示的是第j个指数,所以k每次增i(因为第i个表达式的增量是i)。
⑤ 、把c2的值赋给c1,而把c2初始化为0,因为c2每次是从一个表达式中开始的
咱们赶快趁热打铁,来几道题目:
(相应题目解析均在相应的代码里分析)
1. 题目:http://acm.hdu.edu.cn/showproblem.php?pid=1028
代码:http://www.wutianqi.com/?p=587
这题大家看看简单不?把上面的模板理解了,这题就是小Case!
看看这题:
2. 题目:http://acm.hdu.edu.cn/showproblem.php?pid=1398
代码:http://www.wutianqi.com/?p=590
要说和前一题的区别,就只需要改2个地方。 在i遍历表达式时(可以参考我的资料—《母函数详解》),把i<=nNum改成了i*i<=nNum,其次在k遍历指数时把k+=i变成了k+=i*i; Ok,说来说去还是套模板~~~
3. 题目:http://acm.hdu.edu.cn/showproblem.php?pid=1085
代码:http://www.wutianqi.com/?p=592
这题终于变化了一点,但是万变不离其中。
大家好好分析下,结合代码就会懂了。
4. 题目:http://acm.hdu.edu.cn/showproblem.php?pid=1171
代码:http://www.wutianqi.com/?p=594
还有一些题目,大家有时间自己做做:
HDOJ:1709,1028、1709、1085、1171、1398、2069、2152
判断两条线段是否有交点
#include<iostream>
using namespace std;
typedef struct node
{
double x;
double y;
}Node;
Node star[100],end[100];
int max_dian,N; //N条直线;
double max(double a,double b)
{
return a>b?a:b;
}
double min(double a,double b)
{
return a<b?a:b;
}
double Direction(Node p1,Node p2,Node p) //判断p与线段p1p2 的位置关系,p1当作"原点";
{
return (p2.x-p1.x)*(p.y-p1.y)-(p.x-p1.x)*(p2.y-p1.y);
}
bool online(Node p1,Node p2,Node p) //判断p是否在p1p2上;
{
double max=p1.y>p2.y?p1.y:p2.y;
double min=p1.y>p2.y?p2.y:p1.y;
if( p.y>=min && p.y<=max ) return true;
return false;
}
bool cross(Node p1,Node p2,Node p3,Node p4)
{
double d1=Direction(p1,p2,p3);
double d2=Direction(p1,p2,p4);
double d3=Direction(p3,p4,p1);
double d4=Direction(p3,p4,p2);
if( d1*d2<0 && d3*d4<0 ) return true;
if( d1==0 && online(p1,p2,p3)) return true;//d1=0说明p3在p1_p2上, ;
if( d2==0 && online(p1,p2,p4)) return true;//还要判断是否在线段上
if( d3==0 && online(p3,p4,p1)) return true;
if( d4==0 && online(p3,p4,p2)) return true;
return false;
}
void find_set()
{
max_dian=0;
for(int i=1;i<=N;i++)
{
for(int j=i+1;j<=N;j++)
{
if(cross(star[i],end[i],star[j],end[j]))
max_dian++;
}
}
printf("%d\n",max_dian);
}
int main()
{
freopen("D:\\in.txt","r",stdin);
while( scanf("%d",&N),N)
{
for(int i=1;i<=N;i++)
{
scanf("%lf%lf%lf%lf",&star[i].x,&star[i].y,&end[i].x, &end[i].y);
}
find_set();
}
return 0;
}
nlogn的最长子序列算法
先回顾经典的O(n^2)的动态规划算法,设A[i]表示序列中的第i个数,F[i]表示从1到i这一段中以i结尾的最长上升子序列的长度,初始时设F[i] = 0(i = 1, 2, ..., len(A))。则有动态规划方程:F[i] = max{1, F[j] + 1} (j = 1, 2, ..., i - 1, 且A[j] < A[i])。
现在,我们仔细考虑计算F[i]时的情况。假设有两个元素A[x]和A[y],满足
(1)x < y < i (2)A[x] < A[y] < A[i] (3)F[x] = F[y]
此时,选择F[x]和选择F[y]都可以得到同样的F[i]值,那么,在最长上升子序列的这个位置中,应该选择A[x]还是应该选择A[y]呢?
很明显,选择A[x]比选择A[y]要好。因为由于条件(2),在A[x+1] ... A[i-1]这一段中,如果存在A[z],A[x] < A[z] < a[y],则与选择A[y]相比,将会得到更长的上升子序列。
再根据条件(3),我们会得到一个启示:根据F[]的值进行分类。对于F[]的每一个取值k,我们只需要保留满足F[i] = k的所有A[i]中的最小值。设D[k]记录这个值,即D[k] = min{A[i]} (F[i] = k)。
注意到D[]的两个特点:
(1) D[k]的值是在整个计算过程中是单调不上升的。
(2) D[]的值是有序的,即D[1] < D[2] < D[3] < ... < D[n]。
利用D[],我们可以得到另外一种计算最长上升子序列长度的方法。设当前已经求出的最长上升子序列长度为len。先判断A[i]与D[len]。若A[i] > D[len],则将A[i]接在D[len]后将得到一个更长的上升子序列,len = len + 1, D[len] = A[i];否则,在D[1]..D[len]中,找到最大的j,满足D[j] < A[i]。令k = j + 1,则有D[j] < A[i] <= D[k],将A[i]接在D[j]后将得到一个更长的上升子序列,同时更新D[k] = A[i]。最后,len即为所要求的最长上升子序列的长度。
在上述算法中,若使用朴素的顺序查找在D[1]..D[len]查找,由于共有O(n)个元素需要计算,每次计算时的复杂度是O(n),则整个算法的时间复杂度为O(n^2),与原来的算法相比没有任何进步。但是由于D[]的特点(2),我们在D[]中查找时,可以使用二分查找高效地完成,则整个算法的时间复杂度下降为O(nlogn),有了非常显著的提高。需要注意的是,D[]在算法结束后记录的并不是一个符合题意的最长上升子序列!
这个算法还可以扩展到整个最长子序列系列问题,整个算法的难点在于二分查找的设计,需要非常小心注意。
#i nclude <stdio.h>
#i nclude <string.h>
int D[40000];
int binarysearch(int low,int high,int m)
{
}
int main()
{
}
最大子序列和最大子矩阵
数组的最大子序列问题:给定一个数组,其中元素有正,也有负,找去其中一个连续子序列 , 使
和最大。
这个问题可以可以动态规划的思想解决:
设 b[i]: 表示以第 i 个元素 a[i] 结尾的最大子序列 , 那么显然
b[i+1]=b[i]>0?b[i]+a[i+1]
基于这一点很快就可以完成如下代码 :
int maxSubArray( int ar[], int n)
{
int max = ar[0];
int b = ar[0];
int i;
for (i = 1; i < n; i++)
{
if (b > 0)
b += ar[i];
else
b = ar[i];
if (b > max)
max = b;
}
return max;
}
扩展问题 : 给定一个矩阵 ( 二维数组 ), 其中数据有大有小 , 请找一个子矩阵 , 使得子矩阵的和最大 , 并
输出这个和
同样可以利用动态规划的思想来解决我们想 , 如果确定了选择第 i 列和第 j 列之间的元素 , 那么在这个范围内 , 其实就是一个最大子序
列问题
现在确定第 i 列和第 j 列间,我用的是暴搜
代码如下:
int maxSubMatrix( int (*ma)[4], int m, int n)
{
int i, j, k, max = ma[0][0], tmp;
// 记录每行的和
int * sum = malloc(m * sizeof( int ));
for (i = 0; i < n; i++)
{
for (k = 0; k < m; k++)
sum[k] = 0;
for (j = i; j < n; j++)
{
for (k = 0; k < m; k++)
{
sum[k] += ma[k][j];
}
tmp = maxSubArray(sum, n);
if (tmp > max)
max = tmp;
}
}free(sum);
return max;
}
测试代码
int main()
{
int a[] = {-3, 4, 7, -9, 10, 21, -3, 5, 9};
int ma[3][4] = {-1, 3, 4, 5, 2, 1, 6, 7, 8, -9, -10, 20};
printf(" %d\n ", maxSubArray(a, 9));
printf(" %d\n ", maxSubMatrix(ma, 3, 4));
return 0;
}
SPFA算法
算法思想:我们用数组d记录每个结点的最短路径估计值,用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
代码实现:
#include<iostream>
using namespace std;
struct node
{
int x;
int value;
int next;
};
node e[60000];
int visited[1505],dis[1505],st[1505],queue[1000];
int main()
{
int n,m,u,v,w,start,h,r,cur,i;
while(scanf("%d%d",&n,&m)!=EOF)
{
for(i=1;i<=1500;i++)
{visited[i]=0;
dis[i]=-1;
st[i]=-1;
}
for(i=1;i<=m;i++)
{
scanf("%d%d%d\n",&u,&v,&w);
e[i].x=v;
e[i].value=w;
e[i].next=st[u];
st[u]=i;
}
start=1;
visited[start]=1;
dis[start]=0;
h=0;
r=1;
queue[r]=start;
while(h!=r)
{
h=(h+1)%1000;
cur=queue[h];
int tmp=st[cur];
visited[cur]=0;
while(tmp!=-1)
{
if (dis[e[tmp].x]<dis[cur]+e[tmp].value)
{
dis[e[tmp].x]=dis[cur]+e[tmp].value;
if(visited[e[tmp].x]==0)
{
visited[e[tmp].x]=1;
r=(r+1)%1000;
queue[r]=e[tmp].x;
}
}
tmp=e[tmp].next;
}
}
printf("%d\n",dis[n]);
}
return 0;
}
Hopcroft_Karp算法
在匈牙利算法中,我们每次寻找一条增广路来增加匹配集合M.可以证明,每次找增广路的复杂度是O(E),一共需要增广O(V)次,因此总时间复杂度为O(VE).为了降低时间复杂度,在Hopcroft Karp算法中,我们在增加匹配集合M时,每次寻找多条增广路.可以证明,这样迭代次数最多为2*V^0.5,所以,时间复杂度就降到了O(V^0.5*E).
001 |
#include <iostream> |
002 |
#include <cstdio> |
003 |
#include <cstring> |
004 |
#include <algorithm> |
005 |
#include <vector> |
006 |
#include <cmath> |
007 |
#include <iterator> |
008 |
using namespace std; |
009 |
|
010 |
struct NodeGuest |
011 |
{ |
012 |
double x, y, speed; |
013 |
}ng[3010]; |
014 |
|
015 |
struct NodeNmbre |
016 |
{ |
017 |
double x, y; |
018 |
}nn[3010]; |
019 |
|
020 |
|
021 |
int T, cntGuest, cntUmbre, match; |
022 |
int disx[3010], disy[3010], matx[3010], maty[3010], que[3010]; |
023 |
double t; |
024 |
vector< int > mp[3010]; |
025 |
|
026 |
|
027 |
|
028 |
double getTime( int a, int b) |
029 |
{ |
030 |
double dis = sqrt ( (ng[a].x - nn[b].x) * (ng[a].x - nn[b].x) + (ng[a].y - nn[b].y) * (ng[a].y - nn[b].y)); |
031 |
return dis/ng[a].speed; |
032 |
} |
033 |
|
034 |
|
035 |
bool bfs() |
036 |
{ |
037 |
int i, j, fore = 0, rear = 0; |
038 |
bool flag = false ; |
039 |
vector< int >::iterator ite; |
040 |
for (i=0; i<cntGuest; ++i){ //找出中还没有匹配的点 |
041 |
if (matx[i] == -1) que[rear++] = i; |
042 |
disx[i] = 0; |
043 |
} |
044 |
for (i=0; i<cntUmbre; ++i) disy[i] = 0; |
045 |
while (fore<rear){ //从这些没有匹配的点里,找路径 |
046 |
int x = que[fore++]; |
047 |
for (ite=mp[x].begin(); ite!=mp[x].end(); ++ite){ |
048 |
if (disy[*ite] == 0){ |
049 |
disy[*ite] = disx[x] + 1; |
050 |
if (maty[*ite] == -1) flag = true ; //找到一条路径,则返回值是正的 |
051 |
else { //找不到路径,则把前向点如队列 |
052 |
disx[maty[*ite]] = disy[*ite] + 1; |
053 |
que[rear++] = maty[*ite]; |
054 |
} |
055 |
} |
056 |
} |
057 |
} |
058 |
return flag; |
059 |
} |
060 |
|
061 |
|
062 |
|
063 |
bool dfs( int x) |
064 |
{ |
065 |
vector< int >::iterator ite; |
066 |
for (ite=mp[x].begin(); ite!=mp[x].end(); ++ite){ |
067 |
if (disy[*ite] == disx[x] + 1){ |
068 |
disy[*ite] = 0; |
069 |
if (maty[*ite] == -1 || dfs(maty[*ite])){ |
070 |
matx[x] = *ite; |
071 |
maty[*ite] = x; |
072 |
return 1; |
073 |
} |
074 |
} |
075 |
} |
076 |
return false ; |
077 |
} |
078 |
|
079 |
|
080 |
|
081 |
int Hopcroft_Karp() |
082 |
{ |
083 |
int i; |
084 |
match = 0; |
085 |
memset (matx, -1, sizeof (matx)); |
086 |
memset (maty, -1, sizeof (maty)); |
087 |
while (bfs()){ |
088 |
for (i=0; i<cntGuest; ++i){ |
089 |
if (matx[i] == -1 && dfs(i)) ++match; |
090 |
} |
091 |
} |
092 |
return match; |
093 |
} |
094 |
|
095 |
|
096 |
int main() |
097 |
{ |
098 |
// freopen("c:/aaa.txt", "r", stdin); |
099 |
int ca = 1; |
100 |
scanf ( "%d" , &T); |
101 |
while (T--){ |
102 |
scanf ( "%lf %d" , &t, &cntGuest); |
103 |
int i, j; |
104 |
for (i=0; i<cntGuest; ++i) scanf ( "%lf %lf %lf" , &ng[i].x, &ng[i].y, &ng[i].speed); |
105 |
scanf ( "%d" , &cntUmbre); |
106 |
for (i=0; i<cntUmbre; ++i) scanf ( "%lf %lf" , &nn[i].x, &nn[i].y); |
107 |
for (i=0; i<cntGuest; ++i) mp[i].clear(); |
108 |
for (i=0; i<cntGuest; ++i){ |
109 |
for (j=0; j<cntUmbre; ++j){ |
110 |
double tt = getTime(i, j); |
111 |
if (tt <= t) mp[i].push_back(j); |
112 |
} |
113 |
} |
114 |
|
115 |
printf ( "Scenario #%d:\n%d\n\n" , ca++, Hopcroft_Karp()); |
116 |
} |
117 |
return 0; |
118 |
} |
匈牙利算法___最大匹配
#include<stdio.h> #include<string.h> bool g[201][201]; int n,m,ans; bool b[201]; int link[201]; bool init() { int _x,_y; memset(g,0,sizeof(g)); memset(link,0,sizeof(link)); ans=0; if(scanf("%d%d",&n,&m)==EOF)return false; for(int i=1;i<=n;i++) { scanf("%d",&_x); for(int j=0;j<_x;j++) { scanf("%d",&_y); g[ i ][_y]=true; } } return true; } bool find(int a) { for(int i=1;i<=m;i++) { if(g[a][ i ]==1&&!b[ i ]) { b[ i ]=true; if(link[ i ]==0||find(link[ i ])) { link[ i ]=a; return true; } } } return false; } int main() { while(init()) { for(int i=1;i<=n;i++) { memset(b,0,sizeof(b)); if(find(i))ans++; } printf("%d\n",ans); } }
每次增广时间为O(E),最多进行O(V)次迭代,时间复杂度为O(VE).
素数鰓选法
#include<stdio.h> |
#include<stdlib.h> |
#include<math.h> |
#include<string.h> |
#include<time.h> |
|
int T; |
|
bool Is( int x ) |
{ |
int sum=0; |
for ( int i=1 ;i< x;++i ) |
if ( !(x%i) ) |
sum+=i; |
if ( sum==x ) |
return true ; |
else |
return false ; |
} |
|
int main() |
{ |
scanf ( "%d" ,&T ); |
while (T--) |
{ |
int a,b,cnt=0; |
scanf ( "%d%d" ,&a,&b ); |
if ( a>b ) |
a^=b^=a^=b; |
for ( int i=a ;i<=b;++i ) |
if ( Is( i ) ) |
cnt ++; |
printf ( "%d\n" ,cnt ); |
} |
|
return 0; |
} |