斯坦纳树小结

%%%

参考自:https://www.cnblogs.com/ECJTUACM-873284962/p/7643445.html

定义:

斯坦纳树问题是组合优化学科中的一个问题。将指定点集合中的所有点连通,且边权总和最小的生成树称为最小斯坦纳树(Minimal Steiner Tree),其实最小生成树是最小斯坦纳树的一种特殊情况。而斯坦纳树可以理解为使得指定集合中的点连通的树,但不一定最小。

求解最小斯坦纳树

求解最小斯坦纳树是 N P NP NP问题,没有有效的多项式算法。这个部分个人感觉算是比较生僻的内容,适用范围很局限,大体上是最短路算法和状态压缩转移的结合。

由于题目中给出要求联通的 k k k点集一般比较小,考虑把每个点的联通情况设为状态。设 d p [ i ] [ s t a ] dp[i][sta] dp[i][sta] 为以 i i i 为根,联通状态为 s t a sta sta的最小开销。

考虑如下两种转移

  • 通过子集 s s s进行转移
    则有 d p [ i ] [ s t a ] = m i n ( d p [ i ] [ s ] , d p [ i ] [ s 1 ] + d p [ i ] [ s 2 ] ) dp[i][sta] = min(dp[i][s] ,dp[i][s1] + dp[i][s2]) dp[i][sta]=min(dp[i][s],dp[i][s1]+dp[i][s2]),其中 s 1 ⋂ s 2 = ∅ s1\bigcap s2 = \emptyset s1s2=
    枚举子集可以通过 s n x t = ( s c u r − 1 ) s_{nxt} = (s_{cur} - 1) snxt=(scur1) & s t a sta sta 避免无用状态

  • 通过类似最短路算法的操作进行松弛操作,一般采用 s p f a spfa spfa也见过dijkstra 的写法
    d p [ v ] [ s t a ] = m i n ( d p [ v ] [ s t a ] , d p [ u ] [ s t a ] + c o s t [ u ] [ v ] ) dp[v][sta] = min(dp[v][sta],dp[u][sta] + cost[u][v]) dp[v][sta]=min(dp[v][sta],dp[u][sta]+cost[u][v])

设联通点集大小为 k k k,总点集大小为 n n n,则算法总体复杂度为 O ( n 3 k + 2 k ∗ c ∗ E ) O(n3^k + 2^k * c*E) O(n3k+2kcE)
后面一项为 s p f a spfa spfa 玄学的 复杂度,前一项则可由 ∑ ( k i ) ∗ 2 i = ( 1 + 2 ) k \sum \binom {k}{i}*2^i = (1+2)^k (ik)2i=(1+2)k 推得。

题目:

bzoj 2595: [Wc2008]游览计划

sol:经典斯坦纳树题目
zz题面看了老半天。 题目要求还原路径,需要记录转移过程。这道题中比较特殊的是给的是点权,所以枚举子集的时候需要减掉根的点权,否则根的权值会被统计两次。

code:

#include 
 
using namespace std;
 
typedef long long ll;
const int maxn = 1e5+10;
const int inf = 1e9;
 
#define MP make_pair
#define fi first
#define se second
#define pii pair
 
int dp[11][11][1<<11];
int a[11][11],vis[11][11];
int tot,n,m;
 
vector<pii> run;
 
queue<pii> q;
 
void init(){
    run.resize(4);
    run[0] = MP(-1,0);    run[1] = MP(1,0);
    run[2] = MP(0,-1);    run[3] = MP(0,1);
    memset(dp,0x3f,sizeof(dp));
    tot = 0;
}
 
struct PRE{
    int x,y,S;
}pre[11][11][1<<11];
 
void SPFA(int cur){
    while(!q.empty()){
        pii p = q.front();  q.pop();
        vis[p.fi][p.se] = 0;
        // cout<<"p = "<
        for(int i = 0;i<4;i++){
            int wx = p.fi + run[i].fi,wy = p.se + run[i].se;
            if(wx < 1 || wx > n || wy < 1 || wy > m) continue;
            // cout<<"wx = "<if(dp[wx][wy][cur] > dp[p.fi][p.se][cur] + a[wx][wy]){
                // cout<<"???????????????????"<
                dp[wx][wy][cur] = dp[p.fi][p.se][cur] + a[wx][wy];
                pre[wx][wy][cur] = (PRE){p.fi,p.se,cur};
                if(!vis[wx][wy])
                    vis[wx][wy] = 1,q.push(MP(wx,wy));
            }
        }
    }
}
 
void dfs(int x,int y,int cur){
    // cout<
    vis[x][y] = 1;
    PRE ret = pre[x][y][cur];
    if(ret.x == 0 && ret.y == 0) return ;
    dfs(ret.x,ret.y,ret.S);
    if(ret.x == x && ret.y == y) dfs(ret.x,ret.y,cur - ret.S);
}
 
int main(){
    init();
    scanf("%d%d",&n,&m);
    for(int i = 1;i<=n;i++)
        for(int j = 1;j<=m;j++) {
            scanf("%d",&a[i][j]); 
            if(!a[i][j]) dp[i][j][1<<tot] = 0,tot++; 
        }
    int S = (1 << tot ) - 1;
    // cout<<"tot = "<for(int sta = 0 ;sta<= S;sta++){
        for(int i = 1;i<=n;i++)
            for(int j = 1;j<=m;j++){
                for(int s = sta;s;s = (s-1) & sta){
                    if(dp[i][j][s] + dp[i][j][sta - s] - a[i][j] < dp[i][j][sta])
                        dp[i][j][sta] = dp[i][j][s] + dp[i][j][sta - s] - a[i][j],
                        pre[i][j][sta] = (PRE){i,j,s};
                }
                if(dp[i][j][sta]<inf){
                    // cout<
                    q.push(MP(i,j)),vis[i][j] = 1;
                }
            }
        SPFA(sta);
    }
    int x,y,flag = 0;
    for(int i = 1;i<=n;i++)
        for(int j = 1;j<=m;j++) 
            if(!a[i][j]){
            x = i,y = j;
            flag = 1;
            break;
    }
    // cout<
    printf("%d\n",dp[x][y][S]);
    memset(vis,0,sizeof(vis));
    dfs(x,y,S);
    for(int i = 1;i<=n;i++){
        for(int j = 1;j<=m;j++){
            // cout<<"i j a[i][j] vis[i][j] "<
            if(a[i][j] == 0) putchar('x');
            else if(vis[i][j] == 1) putchar('o');
            else putchar('_');
        }
        puts("");
    }
    return 0;
}


hdu 4085 ( Peach Blossom Spring

sol: 11年北京网络赛,给出n个点,要求前k个点和后k个点一一对应且联通。按斯坦纳树的搞法搞一通后会发现答案可能是个森林,考虑用状压dp处理。题目要求k对点一一对应,则一个合法的联通子集中前k个点和后k个点的数目应该相等。用和上文类似的做法枚举合法的子集的进行转移。
将spfa入队操作写在枚举子集里面,然后T了个爽。

code:

#include 

using namespace std;

typedef long long ll;
const int maxn = 55;
const int inf = 0x3f3f3f3f;

#define MP make_pair
#define fi first
#define se second
#define pdd pair

int f[maxn][1<<12];
int n,k;
int vis[maxn];
int dp[1<<12];

queue<int> Q;

struct edge{
    int v,w;
    edge(int _v,int _w):v(_v),w(_w){}
};
vector<edge> G[maxn];

void SPFA(int cur){
    while(!Q.empty()){
        int u = Q.front(); Q.pop();
        for(int i = 0;i<G[u].size();i++){
            int v = G[u][i].v;
            int w = G[u][i].w;
            if(f[v][cur] > f[u][cur] + w){
                f[v][cur] = f[u][cur] + w;
                if(!vis[v]){
                    vis[v] = 1;
                    Q.push(v);
                }
            }
        }
        vis[u] = 0;
    }
}

bool check(int sta){
    int cnt = 0;
    for(int i = 0;i<k;i++) {
        if(sta & (1<<i)) cnt++;
        if(sta & (1<<(k+i))) cnt--;
    }
    return cnt == 0;
}

int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        int m;
        scanf("%d%d%d",&n,&m,&k);
        for(int i = 0;i<=n;i++) G[i].clear();
        int S = (1<<(k*2)) - 1;
        for(int i = 0;i<=n;i++)
            for(int j = 0;j<=S;j++) f[i][j] = inf;

        while(m--){
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            G[u].push_back(edge{v,w});
            G[v].push_back(edge{u,w});
        }
        for(int i = 1;i<=k;i++){
            f[i][1<<(i-1)] = 0;
            f[n - k + i][1<<(k + i -1)] = 0;
        }

        memset(vis,0,sizeof(vis));
        for(int sta = 0;sta <= S;sta++){
            for(int i = 1;i<=n;i++){
                for(int s = sta;s;s = (s-1) & sta){
                    f[i][sta] = min(f[i][sta],f[i][s] + f[i][sta^s]);

                }   
                if(f[i][sta] < inf){
                    vis[i] = 1;
                    Q.push(i);
                }
            }
            SPFA(sta);
        }

        for(int i = 0;i<=S;i++){
            dp[i] = inf;
            for(int j = 1;j<=n;j++) dp[i] = min(dp[i],f[j][i]);
        }

        for(int sta = 0;sta<=S;sta++){
            if(!check(sta)) continue;
            for(int s = sta;s;s = (s-1) & sta){
                if(!check(s)) continue;
                dp[sta] = min(dp[sta],dp[s] + dp[sta ^ s]);
            }
        }
        if(dp[S] == inf) puts("No solution");
        else printf("%d\n",dp[S]);
    }
    return 0;
}

Gym - 101908J

sol: 巴西区域赛?
限制变成了这k个点只能连出去一条边,也就是说这k个点在树上只能是叶子。为了保证合法性,我们只更新根节点不是这k个点的斯坦纳树。对于这k个点,只允许单个点的联通块更新其他斯坦纳树,保证连出去的边只有一条。有种特殊情况是 k = 2 k=2 k=2的时候,直接连起来就是最优解,但数据规定 k > = 3 k>=3 k>=3规避了种情况,就不用特判了。

#include 

using namespace std;

typedef long long ll;
const int maxn = 105;
const double inf = 1e18;

#define MP make_pair
#define fi first
#define se second
#define pdd pair

double dp[maxn][1<<11];
int tot,n,k;
int vis[maxn];
double len[maxn][maxn];

pdd p[maxn];
queue<int> Q;

inline double dis(pdd a,pdd b){
    return sqrt((a.fi - b.fi) * (a.fi - b.fi) + (a.se - b.se) * (a.se - b.se));
}

void SPFA(int cur){
    while(!Q.empty()){
        int u = Q.front(); Q.pop();
        for(int v = 1;v<=n;v++){
            if(v == u || v <= k) continue;
            if(dp[v][cur] > dp[u][cur] + len[u][v]){
                dp[v][cur] = dp[u][cur] + len[u][v];
                if(!vis[v]){
                    vis[v] = 1;
                    Q.push(v);
                }
            }
        }
        vis[u] = 0;
    }
}

int main(){
    scanf("%d%d",&n,&k);
    int S = (1<<k) - 1;
    for(int i = 1;i<=n;i++)
        for(int s = 0;s <= S; s++) dp[i][s] = inf;
 
    for(int i = 1;i<=k;i++){
        scanf("%lf%lf",&p[i].fi,&p[i].se);
        dp[i][1<<(i-1)] = 0;
    }
    for(int i = k+1;i<=n;i++){
        scanf("%lf%lf",&p[i].fi,&p[i].se);
    }

    for(int i = 1;i<=n;i++)
        for(int j = 1;j<=n;j++) {
            len[i][j] = dis(p[i],p[j]);
        }

    for(int sta = 0;sta <=S;sta++){
        for(int i = k+1;i<=n;i++){
            for(int s = sta;s;s = (s-1) & sta){
                dp[i][sta] = min(dp[i][sta],dp[i][s] + dp[i][sta^s]);
            }
        }
        for(int i = 1;i<=n;i++){
            if(dp[i][sta]<inf - 1) Q.push(i),vis[i] = 1;
        }
        SPFA(sta);
    }

    double ans = inf;
    for(int i = k+1;i<=n;i++) ans = min(ans,dp[i][S]);
    printf("%.5f\n",ans);
    return 0;
}

你可能感兴趣的:(DP)