[CQOI2012]交换棋子 网络流
题解:
一开始很快想出了一个接近正解的建图方法,但其实是错误的,不过还是骗了70分_(:зゝ∠)_
首先我们可以观察到棋子有限,但费用多种,其实也就相当于限制了流量,找最小费用
对于初始状态的每一个1,我们连s ---> x flow = 1 cost = 0
对于目标状态的每一个1,我们连x ---> t flow = 1 cost = 0
对于每一个方块,我们向周围八个格子连边 flow = inf , cost = 1(表示交换了一次)
然后就是比较妙难的部分了
首先我们要拆点,因为每个点有流量限制(交换次数)
我们考虑以下三种情况
1,初始状态是0, 目标状态也是0, 我们连x ----> x' flow = limit/2 cost = 0
那么为什么连的是limit/2,而不是limit呢?
我们可以观察到对于这样一个路径1 ----> x----> 3,夹在中间的x被翻转了两次,但流量却只流经了一次,
所以流一次实际是消耗了2的限制,因此我们直接在建边的时候就建limit/2
2,初始状态是1, 目标状态是0, 我们连x ---> x' flow = (limit + 1) / 2 cost = 0;
那么为什么这里又要+1呢?
因为这里本来就有个棋子,但目标状态却没有,因此这个棋子是必须换出去的。
但是这种交换又和上面的不同了,因为这种情况下,点x在路径中的位置是一个端点,因此是这样的x ----> 2,
于是我们观察到这条路径上,x并没有被翻转两次,流量却和上面一样流经了一次,也就是说如果不做任何修改,
这样虽然只翻转了一次,却还是在被当做翻转了2次对待。这显然是不合理的。
就比如这样的情况:
x ----> 2 其中x的limit是1,那么这个时候x上的那个棋子显然是可以转一次就转出去的,但是如果直接按偶数建就把这次机会给忽略掉了。
3,初始状态是0, 目标状态是1, 我们连x ---> x' flow = (limit - 1) / 2 cost = 0;
为什么这里是-1?
因为这里是别的棋子要进来,进来后因为是目标位置,所以就直接去t了,不会流经x ---> x'
但这显然也是要浪费一个机会的,因此我们在开头就减掉这个1.
总的来说就是用1 的流量表示2个翻转次数,如果流一次会导致多统计1,那我们就在开头加一个1补回来多余的消耗
如果流一次会导致统计不到需要统计的那个1,那我们就在开头-1来表示机会被消耗掉了一个
其实应该也算是人类智慧的一种体现吧,,,强行分类嘛。
这里有一个很巧妙的实现:建边的时候直接给limit加上in[i][j] - out[i][j],具体为什么想想就知道啦,不过我觉得这是个巧合emmmm
不过貌似还有另一种解法,拆3个点,中间的边这样连:
x ---> x' flow = limit/2 , cost = 0
x' ---> x'_ flow = limit/2 + (limit % 2 ? 1 : 0)
当然这只是我的口胡,,,具体正确性有待考证
附代码(有非常详细的注释,,,当然也有一些乱七八糟的注释,,,不过其实我觉得直接看代码应该也能懂_(:зゝ∠)_)
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define R register int 4 #define inf 2139062143 5 #define AC 6000 6 #define ac 80000 7 int n, m, s, all, t, ans, ansflow, cnt, tmp; 8 int date[ac], Head[AC], Next[ac], haveflow[ac], cost[ac], tot = 1; 9 int dis[AC], disflow[AC], last[AC], in[30][30], out[30][30]; 10 int a[11] = {-1, 1, 0, 0, -1, -1, 1, 1}, b[11] = {0, 0, -1, 1, 1, -1, 1, -1}; 11 bool z[AC]; 12 deque<int> q; 13 char ss[30][30]; 14 /*因为原来有,后来没有的格子必须要有一次是经过一次翻转然后出去的,因此对于这种,应该要加1的限制, 15 而原来没有,后来有的格子,必须要有一次是经过一次翻转得到一个棋子的,因此对于这种,应该要-1的限制, 16 这两种之所以不同就是因为原来有,现在没有的格子是要出去棋子,这种情况下肯定会消耗一个流量,且不会有多消耗的风险, 17 而原来没有,后来有的格子,因为是从别的格子进来的,因此无法分辨这到底是要停下的流量,还是要出去的流量, 18 而且完全可能本来是到这里停下的流量跑了出去。这时如果还保留这一个流量,就可能会造成一个点可以翻转两次, 19 但别的棋子进来那次本来就去掉了一次了,却没有在这上面体现出来,因为流量到了这个点就直接去t了, 20 根本不会被计入x ---> x'的管道中。 21 简单来说就是第一种肯定且仅会产生1的流量,而且这个流量将被计入x ---> x'中,因此我们就要多分配1的限制去保证奇数时也可以生效。 22 而第二种肯定且仅会产生1的流量,但这个流量将不会被计入x ---> x'中,但是实际上这个边应当要统计到它,因为它也消耗了一次翻转限制。 23 所以就要人为的减去这个限制,以防止偶数时这次流入打破了偶数的条件,却依然按照偶数来跑*/ 24 inline void add(int f, int w, int S, int C) 25 { 26 date[++tot] = w, Next[tot] = Head[f], haveflow[tot] = S, cost[tot] = C, Head[f] = tot; 27 date[++tot] = f, Next[tot] = Head[w], cost[tot] = -C, Head[w] = tot; 28 // printf("%d ---> %d %d %d\n", f, w, S, C); 29 } 30 31 inline int id(int x, int y) 32 { 33 return (x - 1) * m + y; 34 } 35 //error!!!流进来一次,流出去一次,一共消耗了两个流量! 36 //所以一共块进来最多limit/2次,出去最多limit/2 + 1(单数的话)次(因为不用流进来的消耗) 37 //因此要拆成3个点。,。。。 38 void pre() 39 { 40 int x; 41 scanf("%d%d", &n, &m); 42 all = n * m; 43 s = all * 2 + 1, t = s + 1; 44 for(R i = 1; i <= n; i++) 45 { 46 scanf("%s", ss[i] + 1); 47 for(R j = 1; j <= m; j++) 48 { 49 x = id(i, j); 50 in[i][j] = ss[i][j] - '0'; 51 if(ss[i][j] - '0' > 0) add(s, x, 1, 0), ++tmp; 52 for(R k = 0; k <= 7; k++) 53 if(i + a[k] > 0 && i + a[k] <= n && j + b[k] > 0 && j + b[k] <= m) //8个格子都要连 54 add(x + all, id(i + a[k], j + b[k]), inf, 1);//1 ---> 2 ---> 3这样的路径实际上只有两次翻转,而中间的点将失去两次机会 55 }//因此只需要在1 ---> 2 2 ---> 3这样的路径中记录cost表示一次翻转就可以了,中间流量限制为limit/2即可(因为失去了两次机会) 56 } 57 for(R i = 1; i <= n; i++) 58 { 59 scanf("%s", ss[i] + 1); 60 for(R j = 1; j <= m; j++) 61 { 62 x = id(i, j); 63 out[i][j] = ss[i][j] - '0'; 64 if(ss[i][j] - '0' > 0) 65 add(x, t, 1, 0), ++cnt; 66 //只有原来有,现在没有,也就是要强制移走的时候,才应该给一次机会,多给一个流量让它流走 67 //因为只有这个时候才会出现一条只有两个点的路径,即整条路径上的点都只消耗一的流量,所有点都是端点 68 }//因为当需要在此格停下的时候,不用穿过去实现再一次翻转,从而浪费一些流量,因此连向t的应当是原来的点,而不是拆出来的点 69 } 70 if(cnt != tmp) 71 { 72 printf("-1\n"); 73 exit(0); 74 } 75 for(R i = 1; i <= n; i++) 76 { 77 scanf("%s", ss[i] + 1); 78 for(R j = 1; j <= m; j++) 79 { 80 x = id(i, j); 81 tmp = ss[i][j] - '0'; 82 tmp += in[i][j] - out[i][j];//通过观察可以发现这样的关系所需要的改动刚好就是in[i][j] - out[i][j]的结果 83 if(tmp / 2 > 0) add(x, x + all, tmp / 2, 0); 84 } 85 } 86 } 87 88 void aru() 89 { 90 int x = t; 91 // printf("%d ", t); 92 while(x != s) 93 { 94 haveflow[last[x]] -= disflow[t]; 95 haveflow[last[x] ^ 1] += disflow[t]; 96 x = date[last[x] ^ 1]; 97 // printf("<--- %d ",x); 98 } 99 // printf(" cost = %d\n", dis[t]); 100 ans += disflow[t] * dis[t]; 101 ansflow += disflow[t]; 102 } 103 104 bool spfa() 105 { 106 int x, now; 107 disflow[s] = inf, dis[s] = 0; 108 q.push_front(s), z[s] = true; 109 while(!q.empty()) 110 { 111 x = q.front(); 112 q.pop_front(); 113 z[x] = false; 114 for(R i = Head[x]; i ; i = Next[i]) 115 { 116 now = date[i]; 117 if(haveflow[i] && dis[now] > dis[x] + cost[i]) 118 { 119 dis[now] = dis[x] + cost[i]; 120 disflow[now] = min(disflow[x], haveflow[i]); 121 last[now] = i; 122 if(!z[now] && now != t)//error!!!now不能等于t 123 { 124 z[now] = true; 125 if(!q.empty() && dis[now] < dis[q.front()]) q.push_front(now); 126 else q.push_back(now); 127 } 128 } 129 } 130 } 131 if(dis[t] != inf) aru(); 132 // printf("!!!%d\n", ans); 133 return dis[t] != inf; 134 } 135 136 void work() 137 { 138 memset(dis, 127, sizeof(dis)); 139 while(spfa()) memset(dis, 127, sizeof(dis)); 140 if(ansflow >= cnt) printf("%d\n", ans); 141 else printf("-1\n"); 142 } 143 144 int main() 145 { 146 freopen("in.in","r",stdin); 147 pre(); 148 work(); 149 fclose(stdin); 150 return 0; 151 }