zoukankan      html  css  js  c++  java
  • 「NOIP2017」宝藏

    「NOIP2017」宝藏 题解

    博客阅读效果更佳


    又到了一年一度NOIPCSP-S 赛前复习做真题的时间

    于是就遇上了这道题


    首先观察数据范围 (1 le n le 12) ,那么极大可能性是状压 ( exttt{DP}) 或者 ( exttt{DFS}) 爆搜

    但由于这题放在了 ( exttt{DP}) 列表里面,于是优先考虑状压

    简化题意:

    从给定的 (n) 个点,(m) 条边的有重边的无向联通图中,找出一棵生成树,使得题目所求价值最小

    从题目给出的建边价值来看,我们发现一条边的价值跟以下几点有关:

    • 根的位置
    • 当前状态下的树的高度
    • 该边的长度

    边的长度不能改变,根的位置并不能很好的作为 ( exttt{DP}) 时候的阶段,所以我们考虑以树的高度作为DP的阶段

    设根的深度为1

    ( exttt{f[i][j]}) 表示 当前树的高度为 (i) ,已经选了的点集的集合为 (j),那么状态转移方程即为

    [f[i][j]=min_{k in j}(f[i-1][j mathrm{xor} k]+dis[j mathrm{xor} k][k]cdot(i-1)) ]

    其中异或操作在这里是取补集的意思,( exttt{dis[i][j]}) 表示从 (i) 这个已选点集加上下一层将要选的 (j) 这个点集所需要的最小花费

    那我们应该如何完善 ( exttt{dis}) 数组呢

    先给出递推式

    [dis[i][j]=dis[i][j mathrm{xor} mathrm{lowbit}(j)]+min_{k=1}^{n && (1<<k)&i}(d[log_2mathrm{lowbit}(j)+1][k]) ]

    其中 (d) 数组表示第 (i) 个点到 第 (j) 个点的道路长度(没有则为 (infty) ),(j)(i) 的补集的任一子集

    然后从小到大枚举 (j) ,就能够保证顺序正确(因为 (j mathrm{xor} mathrm{lowbit}(j)) 一定比 (j) 要小)

    因为每一次更新只涉及到一个点的更改,所以不难得出这样预处理 (dis) 数组的正确性

    然后,这题就完了

    另外还有几点需要注意的

    • 边最好使用邻接矩阵储存,因为有重边,而且请不要将初值赋得太大,这样会导致在进行动态规划求解的同时溢出,从而导致答案错误

    • 如果按照上面那种朴素的做法来进行求解复杂度有可能不能承受,观察发现我们枚举了许多不必要的子集,所以我们可以换一个方式:

      for(int i=S;i;i=(i-1)&S)
      

      这样的话所有的 (i) 就一定是 (S) 的子集

      蒟蒻的理解:不等于 (S)(S) 的子集一定在 ([0,S))

      然后或运算可以求出在这当中十进制下数字最大的子集 ,设其为 (P),然后其余所有的十进制表示比他小的子集都在([0,P)) 当中,如此循环求解,自然能够得到所有的子集

    • 关于状态的一点点优化

      容易发现,当树高为 (i) 时,至少需要 (i) 个节点,所以所有状态中点的个数小于 (i) 的(即二进制位上 (1) 的个数小于 (i) 的),全部可以不用枚举子集,直接跳过,这对时间复杂度又有了进一步的常数优化。 这可以通过预处理得到。

    最后贴一下代码,变量名与上面提到的略有不同

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn=12;
    int d[maxn+5][maxn+5];
    int g[maxn+5][(1<<maxn)+5];
    int f[(1<<maxn)+5][(1<<maxn)+5];
    int lg[(1<<maxn)+5];//懒 
    int q[(1<<maxn)+5],cnt;
    int sum[(1<<maxn)+5];
    int main()
    {
    	memset(g,63,sizeof g);
    	ios::sync_with_stdio(0);
    	cin.tie(0),cout.tie(0);
    	int n,m;
    	cin>>n>>m;
    	for(int i=0;i<n;++i)
    		lg[1<<i]=i;//预处理,因为懒
    	for(int i=0;i<12;++i)
    		for(int j=0;j<12;++j)
    			d[i][j]=1000000;//赋最大值
    	for(int i=1;i<=m;++i)
    	{
    		int a,b,c;
    		cin>>a>>b>>c;
    		--a,--b,d[a][b]=d[b][a]=min(d[a][b],c); 
    	}
    	int x,S=(1<<n)-1;//全集定义
    	for(int i=1;i<=S;++i)
    	{
    		x=i;
    		while(x) x&=(x-1),++sum[i];
    	}//预处理每一个状态上点的个数
    	for(int i=1;i<=S;++i)
    	{
    		cnt=0;
    		for(int j=S^i;j;j=(j-1)&(S^i)) q[++cnt]=j;//由于这样做子集的顺序是从大到小的,不符合DP的顺序,所以要逆序 
    		for(int j=cnt;j>=1;--j)
    		{
    			int u=lg[q[j]&-q[j]],e=1000000;
    			for(int v=0;v<n;++v)
    				if(1<<v&i) e=min(d[u][v],e);
    			f[i][q[j]]=f[i][q[j]^(q[j]&-q[j])]+e;
    		}
    	}
    	for(int i=0;i<n;++i) g[1][1<<i]=0;//初始状态
    	for(int i=2;i<=n;++i)
    		for(int j=(1<<i)-1;j<=S;++j)//剪枝,这里i的初始状态跳过了肯定不符合的状态
    		{
    			if(sum[j]<i) continue;//剪枝,不满足直接跳过
    			for(int k=j;k;k=(k-1)&j)
    				g[i][j]=min(g[i][j],g[i-1][j^k]+f[j^k][k]*(i-1));
    		}
    		int ans=(1<<30);
    	for(int i=1;i<=n;++i) ans=min(ans,g[i][S]);//取最小值
    	cout<<ans<<endl;
    	return 0;
    }
    
  • 相关阅读:
    OLEDB SqlHelper
    .net中数据库事务机制
    C#中的@符号
    C#实现WEB服务器
    Snake.Net
    C#在客戶端和服務端操作Excel文件
    数据库连接字符串大全
    获取cpu序列号,硬盘ID,网卡MAC地址
    Asp.net动态生成html页面(、
    document 文挡对象详解
  • 原文地址:https://www.cnblogs.com/HenryHuang-Never-Settle/p/11529900.html
Copyright © 2011-2022 走看看