HDU3038 How Many Answers Are Wrong【巧妙並查集】
題目連結
思路
題意就是說,不停的給你區間和,問你和前面已給出的矛盾的有幾個。
首先,對於給定的一系列區間[a, b],只有有某個點相鄰的區間,我們才能把它結合,得出新的資訊。
像[1, 8], [6, 10]這樣的不相鄰的區間,我們得不出除了他們倆本身以外的任何資訊。
但例如[1, 10], [1,3], [6,10],我們就能把他們結合,得出幾個新區間資訊:[4, 5], [1, 5], [4, 10]。
然後,這時候如果我們把左端點減個1,化為左開右閉的區間,就相當於,我們通過(0, 10], (0, 3], (5, 10]推出了(3, 5], (0, 5], (3, 10],也就是說,我們推出了{0, 3, 5, 10}裡任取兩個當端點的所有區間的資訊。
怎麼樣,是不是有點並查集的味道了?也就是說,只要有端點相同,我們可以把這些點歸到一個集合裡,然後這個集合裡所有的端點,任取兩個,我們都能得出他的資訊。
所以現在我們能做到,對於給定的一個區間(a-1, b],我們可以判斷出,這個區間的資訊能不能通過以前的區間得到,如果不能,把兩個端點所屬的集合合併。如果能,判斷當前給的區間和,是不是和推出來的區間和相等,如果相等沒問題,否則答案++。
那接下來的難點就是,怎麼得到這個推出來的區間和?我們現在只能判斷能不能推出來,但不知道推出來是多少。
這就是這題最巧妙的地方了,我們記一個值w[i],表示並查集裡i這個節點到他父節點的距離,也就是說設sum[i]為(0, i]的區間和,w[i] = sum[i] - sum[fa[i]]。
然後,因為並查集路徑壓縮的特點,每次find之後,每個集合都是成三角形的形狀,所有子節點都直接指向唯一一個父節點。
所以,如果我們能把w[i]維護好,區間(a, b]的和可以很輕鬆的算出來:
sum[b] - sum[a]
= sum[b] - sum[fa[b]] - (sum[a] - sum[fa[a]])
因為fa[b] = fa[a](路徑壓縮的特點),所以
= w[b] - w[a]
那麼接下來就是怎麼維護好這個w了。
首先w發生變動的地方有兩個,一個是find過程中的路徑壓縮,這時候w只要加上他父節點的w值即可。
還有一處就是合併集合的時候,設輸入的集合為(a, b],和為c,a到他集合的父節點的距離為x,b到父節點的距離為y,合併a,b集合時,a所處集合的父節點到b所處的集合的父節點距離為z。
則有等式,x + z - y = c,得z = y - x + c,即z = w[b] - w[a] +c。
至此,本題就完了。
這題並查集真的用的十分巧妙,需要好好理解。
AC程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
typedef long long ll;
#define CLR(x,y) memset((x),(y),sizeof(x))
const int N = 200000 + 100;
int pre[N], w[N];
int find(int x)
{
if (x == pre[x])return x;
else
{
int fa = find(pre[x]);
w[x] += w[pre[x]];
pre[x] = fa;
return fa;
}
}
int main()
{
int n, m;
while (scanf("%d%d", &n, &m) != EOF)
{
for (int i = 0; i <= n; ++i)
{
pre[i] = i;
w[i] = 0;
}
int ans = 0;
while (m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
a--;
int x = find(a), y = find(b);
if (x == y)
{
if (w[b] - w[a] != c)ans++;
}
else
{
pre[y] = x;
w[y] = w[a] - w[b] + c;
}
}
printf("%d\n", ans);
}
return 0;
}