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_n=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),只不过此法实现比较复杂,且时间复杂度相同,使用较少,故略去