这题又是一道混合算法题。
基本思路是用tarjan在无向图中搜出桥边然后缩点将图变成一颗树进行树形DP。
但是这题其实是有平行边的,在双连通tarjan的模板上加一个判断子节点为父亲的语句,让其能通过第二次及以后的边搜到其父亲就能解决平行边问题。
缩点退栈的同时把原来的点所包含的人数全部求出来,求割边的同时也要记录割边两端的节点。
然后根据记录的割边和缩出来的点包含的人数建出新树(其实是图,因为记录的是双向的)。
进行DP时只需要一个DFS从树根(任意选一个缩出来的点)往下搜,每次搜出该子树的人数和,用总人数减去该和的两倍的绝对值就是一个差值,记录最小的那个就是答案了。
注意一点是因为双向建边,DP搜索时要记录父亲,只能往下搜,不能搜该节点的父亲,否则就大错特错。
另外就是没有割边和本身就不是连通图的情况(其实本题一定是连通图)就输出impossible。
附渣代码:
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstdlib>
#define NV 10001
#define NE 20001
using namespace std;
vector<int>orig[NV],newg[NV];
int stack[NV],top;
struct edge{
int u,v;
}ecut[NE];
int ecnt,bcnt,belong[NV];
int dfn[NV],low[NV];
int dex;
const int W = 0;
const int G = 1;
const int B = 2;
int col[NV];
int dp[NV],ans,sum,val[NV];
int n,m;
int min(int a,int b)
{
if(a < b)return a;
return b;
}
void double_tarjan(int u,int f)
{
col[u] = G;
stack[top++] = u;
low[u] = dfn[u] = ++dex;
bool flag = true;
for(int i = 0;i < (int)orig[u].size(); i++){
int v = orig[u][i];
if(v == f && flag){ //避免平行边。
flag = false;
continue;
}
if(col[v] == W){
double_tarjan( v , u );
low[u] = min( low[u] , low[v] );
if(low[v] > dfn[u]){
ecut[++ecnt].u = u; //保存割边。
ecut[ecnt].v = v;
}
}else if(col[v] == G){
low[u] = min( low[u] , dfn[v] );
}
}
col[u] = B;
if(dfn[u] == low[u]){
int v;
bcnt++;
do{
v = stack[--top];
belong[v] = bcnt;
dp[bcnt] += val[v]; //将缩点的原来人数求和。
}while(u != v);
}
}
void Dp(int u,int f){
for(int i = 0;i < (int)newg[u].size(); i++){
int v = newg[u][i];
if( v == f ){ //不能往父亲搜回去,只能往下搜。
continue;
}
Dp( v , u );
dp[u] += dp[v]; //该子树的和。
}
int x = abs( sum - (dp[u] << 1) );
if(x < ans) ans = x; //记录最小答案。
}
void init()
{
top = 0;
dex = 0;
sum = 0;
ans = 0x3fffffff;
bcnt = 0;
ecnt = 0;
for(int i = 0;i <= n; i++){
orig[i].clear();
newg[i].clear();
dp[i] = 0;
dfn[i] = 0;
col[i] = W;
}
}
int main()
{
while(scanf("%d%d",&n,&m) != EOF){
init();
for(int i = 1;i <= n; i++){
scanf("%d",val + i);
sum += val[i];
}
for(int i = 1;i <= m; i++){
int p1,p2;
scanf("%d%d",&p1,&p2);
p1++;p2++; //统一点的编号从1开始。
orig[p1].push_back(p2);
orig[p2].push_back(p1);//双向建边。
}
int cs = 0;
for(int i = 1;i <= n; i++){
if(dfn[i] == 0){
double_tarjan( i , 0 );
cs++; //记录原图是否连通。
}
}
if( ecnt == 0 || cs > 1){//如果割边数目为零或者原图是由多个连通图组成的
printf("impossible\n");//就输出impossible。
continue;
}
for(int i = 1;i <= ecnt; i++){
int p1 = belong[ecut[i].u];//将保存的割边和两端所属的缩点建成新的无向图。
int p2 = belong[ecut[i].v];
if(p1 != p2){
newg[p1].push_back(p2);
newg[p2].push_back(p1);
}
}
Dp( 1 , 0 ); //进行DP。
printf("%d\n",ans);
}
return 0;
}