好孩子's Blog

母函数标本

母函数

第一种:

有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的最长子序列算法

这是一个很好的题目。题目的算法还是比较容易看出来的,就是求最长上升子序列的长度。不过这一题的数据规模最大可以达到40000,经典的O(n^2)的动态规划算法明显会超时。我们需要寻找更好的方法来解决是最长上升子序列问题。

  先回顾经典的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 mid;
    mid=(low+high)/2;
    while(low<=high)
    {
        if(D[mid]<m && D[mid+1]>=m) return mid;
        else if(D[mid]<m) low=mid+1;
        else high=mid-1;
        mid=(low+high)/2;
    
    return mid;
   
int main()
{
    int a[40000];
    int kase,n,i,j,k,len;
    scanf("%d",&kase);
    while(kase--)
    {
        scanf("%d",&n);
        for(i=1;i<=n;i++) scanf("%d",&a[i]);
        //memset(D,0,sizeof(D));
        D[1]=a[1];
        len=1;
        for(i=2;i<=n;i++)
        {
            if(a[i]>D[len]){len++;D[len]=a[i];}
            else
            {
                j=binarysearch(1,len,a[i]);
                k=j+1;
                D[k]=a[i];
            
         
         printf("%d\n",len);   
    }
    return 1;
}

 

最大子序列和最大子矩阵

数组的最大子序列问题:给定一个数组,其中元素有正,也有负,找去其中一个连续子序列 , 使
和最大。
这个问题可以可以动态规划的思想解决:
设 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算法

  

适用范围给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。 我们约定有向加权图G不存在负权回路,即最短路径一定存在。当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路,但这不是我们讨论的重点。
 

算法思想:我们用数组d记录每个结点的最短路径估计值,用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

  
证明:
  每次将点放入队尾,都是经过松弛操作达到的。换言之,每次的优化将会有某个点v的最短路径估计值d[v]变小。所以算法的执行会使d越来越小。由于我们假定图中不存在负权回路,所以每个结点都有最短路径值。因此,算法不会无限执行下去,随着d的逐渐变小,直到到达最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值

 

期望的时间复杂度O(ke) 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2
 
实现方法:
 
  建立一个队列,初始时队列里只有起始点,建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点作为起始点去刷新到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空
 
判断有无负环
  如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)
 

 

对于图中的每个顶点vV,都设置一个属性d[v],描述从源点sv的最短路径上权值的上界,称为最短路径估计。pre[v]代表Sv的当前最短路径中v点之前的一个点的编,我们用下面的过程来对最短路径估计和前趋进行初始化。(Θ(V)时间)。经过初始化以后,对所有vVpre[v]=NULL,对vV-{s},有d[s]=0以及d[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).

hdoj 2389  Rain on your Parade

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;
}