JS禁止查看网页源代码的简单实现方法

【算法总结】图论/dp-动态规划 大总结

写于一只蹲在角落的蒟蒻-Z__X...
2020.2.7,图论和 \(dp\) 终于告一段落。蓦然回首,好似已走过许多...不曾细细品味,太多太多又绵延不断地向我涌来...
谨以此纪念 逝去 的图论和 \(dp\);

图论

  • 图的存储

    首先,图论的基础:存储。这里介绍几种存储结构;
    邻接矩阵
    一种最简单,暴力的存储结构,二维数组存储;
    注:这是读入方式的一种,具体看题目。

     	cin >> n >> m;
    	for (int i=1;i=m;i++)
    	{
    		cin >> i >> j >> x;   
    		a[i][j]=a[j][i]=x;
     	}
    

    邻接表(链式前向星)
    邻接表,又叫链式前向星,其实就是链表的思路;
    先开一个 \(linkk\) 数组,\(linkk[i]\) 表示的是以 \(i\) 为起点第一条边的编号,\(e\) 数组存边,\(e[i].y\) 表示终点,\(e[i].v\) 表示权值,\(e[i].next\) 表示下条边的编号;
    邻接表核心就是一个插入函数:

    void insert(int x,int y,int v)  //x为起点,y为终点,v为权值。
    {  
      e[++t].y=y; e[t].v=v;
      e[t].next=linkk[x]; linkk[x]=t;
    }
    

    还有一个循环同样重要,类似于查询:

    for (int i=linkk[x];i;i=e[i].next)
    

    边表(边集数组)
    一种简便的存储结构,思路同样很简单,就是把所有的边存储到 \(e\) 数组中,要存储起点,终点,权值。

    struct  node
    {
    	int x,y;  //起点和终点
    	int v;    //权值
    }e[maxm];
    
  • 图的遍历

    dfs遍历
    邻接矩阵 \(dfs\) 遍历:

    void dfs(int k);
    {
    	printf("%d",k);
    	f[k]=true;
    	for (int i=1;i<=n;i++)
         if (!f[i] && a[k][i])  dfs(i);
    }
    

    邻接表 \(dfs\) 遍历:

    void dfs(int k)
    {
    	for (int i=linkk[k];i;i=e[i].next)
    	 if(!vis[e[i].y]) 
    	 {
    	 	vis[e[i].y]=1;
    	 	dfs(e[i].y);
       }
    }
    

    bfs遍历
    邻接矩阵 \(bfs\) 遍历:

    void  bfs(int i);
    {
    	memset(q,0,sizeof(q));
    	int head=1,tail=1;
    	q[1]=i; f[i]=true;
    	while (head<=tail)
    	{
    		k=q[head]; cout>>k;
        	for (int j=1;j<=n,j++)
             if (a[k][j] && !f[j])
             {
      	   		q[++tail]=j;
       			f[j]=true;
             }
            head++;
      	 }	
    }
    
  • 最短路

    Floyd
    \(k\) 为中转点,更新 \(i\)\(j\) 的最短路

    for (int k=1;k<=n;k++)
      for (int i=1;i<=n;i++)
        for (int j=1;j<=n;j++)
          if  (dis[i][k]+dis[k][j]<dis[i][j])
    	    dis[i][j]=dis[i][k]+dis[k][j];
    

    Dijkstra
    每次在未求点中找一个离起点距离最短的点,标记,并用这个点作为中转点更新未标记的点的最短路
    注意:\(Dijkstra\) 不支持负权

    void dijkstra(int st);
    {
    	memset(f,0,sizeof(f));
    	memset(dis,0,sizeof(dis));
    	for (int i=1;i<=n;i++) dis[i]=a[st][i];
    	f[st]=1;  dis[st]=0;
    	for (int i=1;i<n;i++)
    	{
        	int min=0xfffffff, k=0;
        	for (int j=1;j<=n;j++)
            	if (!f[j] && (dis[j]<min))  min=dis[j],k=j;
        	if (k==0)  return;  //找不到距离最短的点了
        	f[k]=1;  //把k加入集合1;
        	for (int j=1;j<=n;j++)  //三角形迭代更新最短距离
             if (!f[j] && dis[k]+a[k][j]<dis[j])  dis[j]=dis[k]+a[k][j];      
    	}
    }
    

    Bellman-ford
    支持负环

    int bellman_ford()
    {
    	memset(dis,10,sizeof(dis));  //初值
    	dis[1]=0;  //起点到起点的距离为0
    	for (int i=1;i<=n;i++)  //最多迭代n次
    	{
    		bool p=0;  //是否有松弛标记
    		for (int j=1;j<=tot;j++)  //tot条边
    		{
    			int ax=a[j].x,ay=a[j].y,av=a[j].v;  //第j条边起点,终点,长度
    			if (dis[ax]+av<dis[ay])  //三角形迭代
    			{
    				dis[ay]=dis[ax]+av;
    				p=1;  //松弛标记
    			}
    		}
    		if (p==0) return 0;  //无松弛
    	}
    	return 1;  //有负环
    }
    

    SPFA
    \(Bellman-ford\) 的升级版,对 \(Bellman-ford\) 的迭代进行了改进,每次只要从上次刚被“松驰”过的点 \(x\),来看看 $x¥ 能不能松驰其它点即可,用BFS中的队列来存放刚被“松驰”过的点;

    void SPFA(int k)  //SPFA模板
    {
    	memset(dis,10,sizeof(dis));  //初值
    	memset(vis,0,sizeof(vis));  //清空标记数组,vis[k]表示点k是否在队列中
    	dis[k]=0;
    	vis[k]=1;
    	int head=1,tail=1;
    	q[head]=k;  //点k进队
    	for (head=1;head<=tail;head++)
    	{
    		int x=q[head];  //取出队头
    		for (int i=linkk[x];i>0;i=e[i].next)  //邻接表查询
    		{
    			if (dis[x]+e[i].v<dis[e[i].y])  //迭代
    			{
    				dis[e[i].y]=dis[x]+e[i].v;
    				if (vis[e[i].y]==0)  //若此点没有标记,标记,进队
    				{
    					vis[e[i].y]=1;
    					q[++tail]=e[i].y;
    				}
    			}
    		}
    		vis[x]=0;  //出队
    	}
    }
    
  • 最小生成树

    Prim
    注意:这边给出的代码求的是最小生成树的边权和

    void prim(int st)  //prim算法
    {
    	memset(vis,0,sizeof(vis));
    	memset(dis,0,sizeof(dis));
    	for (int i=1;i<=n;i++) dis[i]=a[st][i];  //赋值交叉边
    	vis[st]=1; int ans=-1e9;
    	for (int i=1;i<=n-1;i++)
    	{
    		int Min=1e9,mini=0;
    		for (int j=1;j<=n;j++)
    		 if (!vis[j] && dis[j]<Min) Min=dis[j],mini=j;  //找到距离最近的点
    		vis[mini]=1;  //标记
    		ans+=dis[mini];  //累加边权和
    		for (int j=1;j<=n;j++)
    		 if (!vis[j] && dis[j]>a[mini][j])
    		 {
    		 	dis[j]=a[mini][j];  //迭代
    		 }
    	}
    	printf("%d",ans);
    }
    

    Kruskal
    按边权从小到大排序,然后从小到大依次取边,需要用到并查集的算法判断加入边 \((x,y)\) 是否形成环,如果有环,就放弃此边,否则加入边 \((x,y)\)。如果已经加入了 \(n-1\) 条边,结束;
    \(Kruskal\) 算法最难点在于怎样判断加入边 \((x,y)\) 后是否形成了环,判断边 \((x,y)\) 的两个顶点 x,y 在图中是否已经连通,在并查集中只要直接判断他们的父亲是否相同就可以了。

    void Kruskal(int k)  //克鲁斯卡尔
    {
    	for (int i=1;i<=n;i++) father[i]=i;  //开始每个点的父节点都是它本身
    	int cnt=0;
    	for (int i=1;i<=m;i++)
    	{
    		int v=getfather(e[i].x);
    		int u=getfather(e[i].y);
    		if (v!=u)  //判断e[i].x和e[i].y是否存在于同一集合内
    		{
    			merge(u,v);  //合并
    			ans+=e[i].v;
    			if (++cnt==n-1)  //最小生成树生成结束
    			{
    				break;
    			}
    		}
    	}
    }
    
  • 拓扑排序

    对有向无环图的点进行先后排序;
    这里用队列来维护拓扑序

    void Topsort()  //拓扑排序
    {
    	int head=1,tail=0;
    	for (int i=1;i<=n;i++)
      	  if (in[i]==0) q[++tail]=i;  //初始化
    	for (head=1;head<=tail;head++)
    	{
    		int x=q[head];  //取出队头
    		for (int i=linkk[x];i;i=e[i].next)  //邻接表查询
    		{
    			int y=e[i].y;
    			if (--in[y]==0) q[++tail]=y;  //入队
    		}
    		if (tail==n) return;
    	}
    }
    
    注意:
    1. 有向图的拓扑序列不唯一;
    2. 如果图中存在环,无拓扑序列;
    3. 还有一种写法用栈来实现拓扑排序。
    

dp

  • dp入门

    背包
    具体戳这儿
    递推
    这里给一个例子,不再多说
    给定一个由 \(n\) 行数字组成的数字三角形,如下图所示:
       7
       3 8
       8 1 0
       2 7 4 4
       4 5 2 6 5

    试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大(每一步只能从一个数走到下一层上和它最近的左边的数或者右边的数)。

    	for (int i=1;i<=n;i++)
     	 for (int j=1;j<=i;j++)
    	 {
     		cin >> a[i][j];
    	 }
    	for (int i=1;i<=n;i++)
    	 for (int j=1;j<=i;j++)
    	 {
     		f[i][j]=max(f[i-1][j],f[i-1][j-1])+a[i][j];
    	 } 
    

    LCS-最长不上升子序列问题、LIS-最长不下降子序列问题
    有一道经典的题目囊括了这两个问题:导弹拦截。
    为了拦截敌国的袭击,科学家研发出一套导弹系统,导弹系统有个缺陷:第一发炮弹可以到达任意高度,然而之后的每一发炮弹都不能高于前一发的高度。
    现给出数个导弹的高度 \(h[i]\) ( \(h[i]\)\(<=50000\)的正整数 ),计算一套导弹拦截系统最多可以拦截多少导弹,如果需要拦截全部导弹需要多少套导弹拦截系统?
    思路:
    第一问很显然,就是一个 \(LCS\) 的模板;
    第二问是一个 \(LIS\) 的模板,为什么呢?(蒟蒻不会 呜呜呜~)

    	for (int i=1;i<=n;i++) //LCS
        {
      		dp[i]=1;
      		for (int j=0;j<=i-1;j++)
      	  	  if (h[i]<=h[j]) dp[i]=max(dp[i],dp[j]+1);
      	}
      	int Max=-0xfffffff;
      	for (int i=1;i<=n;i++) Max=max(Max,dp[i]);
      	cout << Max << endl;
    	for (int i=1;i<=n;i++) //LIS
      	{
      		dp[i]=1;
      		for (int j=0;j<=i-1;j++)
      		  if (h[i]>h[j]) dp[i]=max(dp[i],dp[j]+1);
      	}
      	Max=-0xfffffff;
      	for (int i=1;i<=n;i++) Max=max(Max,dp[i]);
      	cout << Max << endl;
    
  • 区间dp

    好像就是一个板子套用一下吧,这里就列一下最经典的石子合并问题。
    在一个圆形操场的四周摆放 \(N\) 堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的 \(2\) 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
    试设计出一个算法,计算出将 \(N\) 堆石子合并成 \(1\) 堆的最小得分和最大得分。

    	for (int i=1;i<=n;i++)
      	{
      		cin >> a[i];
      		sum[i]=sum[i-1]+a[i];  //前缀和,前i个石子堆的总数量
      	}
      	memset(f,10,sizeof(f));
      	for (int i=1;i<=n;i++) f[i][i]=0;
      	for (int len=2;len<=n;len++)  //区间长度
      	{
      		for (int i=1;i<=n-len+1;i++)  //左端点
      		{
      			int j=i+len-1;  //右端点
      			for (int k=i;k<=j;k++)
      		 	 f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);  //i到j的区间最优值通过中间点k来更新
      		}
      	}
      	cout << f[1][n];
    
  • 二维dp

    二维 \(dp\),最经典的应该还是马拦过河卒了吧,这边拿这道题做例子;
    棋盘上 \(A\) 点有一个过河卒,需要走到目标 \(B\) 点。卒行走的规则:可以向下、或者向右。
    同时在棋盘上 \(C\) 点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为“马拦过河卒”。
    棋盘用坐标表示,\(A\)\((0, 0)\)\(B\)\((n, m)(n, m\)为不超过 \(15\) 的整数\()\),同样马的位置坐标是需要给出的。现在要求你计算出卒从 \(A\) 点能够到达 \(B\) 点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。

    	scanf("%d %d %d %d",&n,&m,&x,&y);
      	n++;m++;x++;y++;
      	memset(f,0,sizeof(f));
      	for (int i=0;i<=8;i++)
      	{
    		int xx=x+fx[i],yy=y+fy[i];
      		if (xx>=1 && xx<=n && yy>=1 && yy<=m)
      		{
      			f[xx][yy]=1;
      		}
      	}
      	dp[1][1]=1;
      	for (int i=1;i<=n;i++)
      	 for (int j=1;j<=m;j++)
       	  if (f[i][j]==0 && !(i==1 && j==1))
       	  {
    		  dp[i][j]=dp[i-1][j]+dp[i][j-1];
    	  }
      	printf("%d",dp[n][m]);
    
  • 双进程dp

    emm...以最长公共子序列为例吧。
    字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。
    令给定的字符序列\(X=“x0,x1,…,xm-1”\),序列\(Y=“y0,y1,…,yk-1”\)\(X\)的子序列,存在X的一个严格递增下标序列,使得对所有的\(j=0,1,…,k-1,有xij = yj\)
    例如,\(X=“ABCBDAB”\)\(Y=“BCDB”\)\(X\)的一个子序列。
    对给定的两个字符序列,求出他们最长的公共子序列。

	while (cin >> ch,ch!='.') a[++n]=ch;
	while (cin >> ch,ch!='.') b[++m]=ch;
	for (int i=1;i<=n;i++)
	 for (int j=1;j<=m;j++)
	 {
	 	dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
	 	if (a[i]==b[j]) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
	 }
	cout << dp[n][m];
posted @ 2020-02-24 17:42  Z__X  阅读(380)  评论(0编辑  收藏  举报