Connect

  你们敢信我看完题解后一边打对??真的连调都没调,虽然后来证明\(TLE\)
  数据范围蛮小的,所以可以用邻接矩阵存边,这样方便查询边权,
  这题是状压\(DP\)
  从1到\(n\)只有一条路径,那么就是一条链。
  其他的联通块最多只能与链上的的点有一条连边,多了会成环,造成不只一条路径。
  记c[i][s]为当前链延伸到\(i\)为端点,已经考虑过的点的集合为\(s\)的最小花费。
  其中\(s\)不仅包含链上的点,也包含与链通过边相连的联通块中的点。
  转移分为两个方面:

1:向端点连一个点作为新的端点他的代价是山删去所有除了(u,v)以外所有边,u为当前的端点,v为要加入的点。
为什么是所有呢,其实不会有关系,因为其他与v相连的不在链上的的点会在将他的联通块加入时将与他相连的的边的权重新加回来。
2:在链端加入一个联通块,代价是删去所有除了不与u相连的边。

  详细方程见代码。
  虽然一个联通块可能被加在链的多个位置,但考虑当前\(DP\)的端点有可能之后不是端点,那么只考虑向当前端点加入联通块就可以遍历所有状态。
  最后,虽然可以用邻接矩阵,但是复杂度会高,本机跑了一秒多(大数据)。
  后来用前向星优化枚举,本机卡进了一秒。
  然而他在\(OJ\)\(T\)了,是OJ太垃还是本机不可信??
  后来想到了\(lowbit\)这个东西,本以为不会优化太多,结果从0.9直接优化到了0.32。
  真香。。。。。

Code
#include<bits/stdc++.h>
using namespace std;
namespace STD
{
	#define rr register
	typedef long long ll;
	const int N=17;
	const int M=2000;
	int n,m;
	int w[N][N],sum[N][1<<15];
	int c[N][1<<15];
	int to[M<<1],dire[M<<1],head[N];
	int mp[1<<15];
	template<typename T> inline T cmin(rr T x,rr T y){return x<y?x:y;}
	static char buf[100000],*pa=buf,*pd=buf;
	#define gc pa==pd&&(pd=(pa=buf)+fread(buf,1,100000,stdin),pa==pd)?EOF:*pa++
	inline int read()
	{
		register int x(0);register char c(gc);
		while(c<'0'||c>'9')c=gc;
		while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=gc;
		return x;
	}
	void pre()
	{
		/*
			两个关系:
				一个是元素与集合的关系
				一个是集合与集合的关系
		*/
		for(rr int i=1;i<=n;i++)
			for(rr int j=1;j<(1<<n);j++)
			{
				int x=j;
				int num=1;
				while(x)
				{
					if(x&1) sum[i][j]+=w[i][num];
					num++;
					x>>=1;
				}
			}	
		for(rr int i=1;i<=n;i++)
			mp[1<<(i-1)]=i;
	}
};
using namespace STD;
int main() 
{
	memset(c,127,sizeof c);
	n=read(),m=read();
	int cnt=0;
	for(rr int i=1;i<=m;i++)
	{
		rr int x=read(),y=read();
		w[x][y]=w[y][x]=read();
		to[++cnt]=y;
		dire[cnt]=head[x];
		head[x]=cnt;
		to[++cnt]=x;
		dire[cnt]=head[y];
		head[y]=cnt;
	}
	pre();
	c[1][1]=0;
	for(rr int i=1;i<=n;i++)//现在的链端
		for(rr int con=1;con<(1<<n);con++)//已经考虑的点集
		{
			if(c[i][con]==c[0][0])continue;
			if(!(con&(1<<(i-1)))) continue;
			for(rr int j=head[i];j;j=dire[j])//要转移向谁
				c[to[j]][con|(1<<(to[j]-1))]=cmin(c[to[j]][con|(1<<(to[j]-1))],c[i][con]+sum[to[j]][con]-w[i][to[j]]);
			//枚举当前集合补集的子集
			int C=((1<<n)-1)^con;
			for(rr int s=C;s;s=((s-1)&C))	
			{
				int temp=0;	
				int x=s;
				int num=1;
				//continue;
				while(x)
				{
					temp+=sum[mp[x&(-x)]][con]-w[mp[x&(-x)]][i];
					x-=(x&(-x));
				}
				c[i][s|con]=cmin(c[i][s|con],c[i][con]+temp);
			}
		}
	printf("%d\n",c[n][(1<<n)-1]);
}

  另外还学到了子集枚举的正确打开方式:

for(rr int t=s;t;t=(t-1)&s)
posted @ 2021-08-08 20:38  Geek_kay  阅读(319)  评论(0编辑  收藏  举报