DP的环形and后效性处理

环形与后效性处理

环形处理:

即我们需要在一个环上进行DP

这种问题一般有两种处理方法

1.执行2次DP,在第一次DP时将问题随便找个点断开当成线性问题处理,第二次DP的时候通过对DP初始值的适当赋值,以及方程式及转移做出些许更改,保证计算出的代价等于把断开的点强制连上

2.将环断开,再复制一倍在末尾此时长度大于n的我们就不需要去考虑

两种方法要适当选择

第一种方法具有一定的局限性,即很多时候我们无法保证,这时候再采用法2,一般来讲在不付出额外代价的时候,方法一是更加优秀的

后效性处理

在一些题目中,题目解法神似DP,但却在我们列出了状态转移方程之后发现了有后效性,此做法就是关于后效性的处理:

使用前提:DP的三要素三前提只有无后效性不能满足,并且转移方程都是一次式,这样我们就可以将状态看作一个个未知数,通过状态转移方程的关系式直接用\(Guass\)消元求出方程的解。

但是一般题目不会如此直白(不然不就不考DP了嘛),一般都是整体框架是DP,后效性只是局部型带环,这时候我们就需要大方向使用DP,在状态转移的时候才使用\(Guass\)消元求解

需要注意的是,因为状态转移方程都是简单的一次式,所以我们应该手动在草稿纸上将其移项,然后根据信息规律直接在代码里手动构建系数矩阵等等

例题:XOR和路径

题意简述:给定一个带权无向连通图(边权),上面有\(1\sim n\)个节点,从1开始编号。从 1 号节点开始,以相等的概率,随机选择与当前节点相关联的某条边,并沿着这条边走到下一个节点,重复这个过程直到走到 N 号节点为止,问一路上边权的异或和期望是多少

分析:发现由于异或的性质,我们直接处理并不怎么好处理,于是因为异或的按位不变性,以及数学期望是线性函数,我们可以对这个期望值进行按位计算

于是我们很容易可以得到状态转移方程:

\(F_u\)表示在当前位下,\(u\to n\)的路径上异或值为\(1\)的概率,设\(v\)\(u\)的子节点,\(deg_u\)表示\(u\)与u相连节点个数(即度)则

\[F_u=\left(\frac{1}{deg_u}\sum_{w(u,v)=0}F_v\right) + \left(\frac{1}{deg_u}\sum_{w(u,v)=1}\left(1-F_v\right)\right) \]

边界:\(F_n=1\)

因为这是一个无向连通图,所以这个状态转移方程是肯定有后效性的,这时候就需要我们上文说的高斯消元了

对这个式子进行一下变式,可得:

\[deg_u\times F_u+\sum_{w(u,v)=0}F_v-\sum_{w(u,v)=1}F_v=\sum_{w(u,v)=1}1 \]

因为题目数据范围中\(n \le 100\),所以我们可以将每一个\(F\)值全部作为未知数,这样我们按照状态转移方程就可以得到\(n\)\(n\)元一次方程,运用高斯消元解即可

最后由于我们是按位计算的,所以第\(i\)位(从第0位开始)对答案的贡献是\(2^i\times F_1\)

具体实现:

#define int long long
#define db double 
db a[105][105],ans=0.0,eps=1e-8;
int tot,head[205],nxt[20005],ver[20005],cost[20005],deg[10005],n,m;
void add(int u,int v,int w){
	deg[u]++,nxt[++tot]=head[u],cost[tot]=w,ver[tot]=v,head[u]=tot;
}
db fabs(db a){
	return a<0?-a:a;
}
void swap(db &a,db &b){
	db c=aa=b,b=c;
} 
void init(int bit){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n+1;j++)a[i][j]=0.0;
	}
	a[n][n]=1;
	for(int u=1;u<n;u++){
		a[u][u]=deg[u];
		for(int i=head[u];i;i=nxt[i]){
			int v=ver[i];
			if(cost[i]>>bit&1)a[u][n+1]++,a[u][v]++;
			else a[u][v]-- ;
		}
	} 
}
void Guass(){
	for(int i=1;i<=n;i++){
		int p=0;
		for(int j=i;j<=n;j++){
			if(fabs(a[j][i])>eps){
				p=j	;
				break;
			}
		}
		if(!p)continue;
		for(int j=1;j<=n+1;j++)swap(a[i][j],a[p][j]);
		for(int j=1;j<=n;j++){
			if(j!=i){
				db rote=a[j][i]/a[i][i];
				for(int k=i;k<=n+1;k++){
					a[j][k]-=a[i][k]*rote;
				} 
			}
		}
	}
}
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++){
		int u,v,w;
		scanf("%lld%lld%lld",&u,&v,&w);
		add(u,v,w);
		if(u!=v)add(v,u,w);
	}
	for(int i=0;i<=50;i++){
		init(i);
		Guass();
		ans+=((1<<i)*a[1][n+1])/a[1][1];
	}
	printf("%.3lf",ans);
}

还有就是我们的状态转移方程很多时候会很特殊(因为高斯消元是\(O(n^3)\)的算法,从数据范围就可以发现端倪),这些系数矩阵列出来会比较特殊,比如上三角矩阵,只有对角线附近有元素的矩阵,或者说元素分布比较集中且每个方程里的未知数较少的时候,我们就可以对高斯消元算法进行一些改造,以此来加快速度

最后说一个深入理解的东西:即我们的DP其实都是对状态空间的遍历,而我们的DP如果是具有后效性其实就可以看作状态空间中存在环(本题中是一维DP,故状态空间是一张图),而我们的高斯消元就是从代数层面去找环间的关系求出值,而如果从几何图论角度去看,我们也可以用找环的方式去消去后效性(SPFA),只不过此法实现比较复杂,且时间复杂度相同,使用较少,故略去

posted @ 2022-11-30 22:35  spdarkle  阅读(194)  评论(0编辑  收藏  举报