洛谷題單指南-字首和差分與離散化-P5937 [CEOI1999] Parity Game

五月江城發表於2024-08-12

原題連結:https://www.luogu.com.cn/problem/P5937

題意解讀:已知長度為n的01序列,給出m個判斷,每個判斷認為l~r之間1的個數是偶數或者奇數,計算前多少個判斷是正確的。

解題思路:

先用字首和思想來思考本題:假設s[i]是序列前i個數的和

對於每一個判斷,有兩種可能

第一、l~r有偶數個1,則有s[r]-s[l-1]是偶數,則有s[r]與s[l-1]同奇或者同偶

第二、l~r有奇數個1,則有s[r]-l[l-1]是奇數,則有s[r]與s[l-1]奇偶性不同

那麼問題就轉換成了,給定m個判斷,每個判斷是關於l-1和r之間的奇偶性關係,計算前多少個判斷是正確的。

由於l、r的取值範圍較大,首先需要進行離散化處理

1、離散化

struct node
{
    int l, r;
    string type;
} a[N];
int s[N * 2], cnt; //用於離散化的陣列
map<int, int> h; //用於離散化的map
//離散化
int idx = 0;
sort(s + 1, s + cnt + 1);
for(int i = 1; i <= cnt; i++)
{
    if(!h.count(s[i])) h[s[i]] = ++idx;
}
for(int i = 1; i <= m; i++)
{
    a[i].l = h[a[i].l];
    a[i].r = h[a[i].r];
}

要處理元素之間的關係,並判斷關係是否正確,可以藉助於並查集,通常有兩種方法:帶權並查集、擴充套件域並查集。

2、帶權並查集

所謂帶權並查集,就是在並查集中維護元素與其根節點之間的權值,可以透過一個陣列d[N]來表示,d[x]表示節點x到父節點p[x]的權值

如何透過權值表示節點之間的關係呢?

比如:設元素x,d[x]=0表示x與其父節點奇偶性相同,d[x]=1表示x與其父節點奇偶性不同

d[x]在並查集路徑壓縮時要進行更新:

int find(int x)
{
    if(p[x] != x)
    {
        int tmp = find(p[x]);
        d[x] = (d[x] + d[p[x]]) % 2; //把d[x]表示到父節點的權值更新為到根節點的權值,由於權值只有0和1,所以%2
        p[x] = tmp;
    }
    return p[x];
}

知道了每個元素與其父節點之間的關係,那麼兩個元素之間的關係如何確定呢?

如果x,y在同一個集合中,d[x]、d[y]已經在路徑壓縮是更新成x、y到根節點的權值,有以下4點結論:

a、如果x與根節點奇偶性相同,y與根節點奇偶性相同,那麼x,y奇偶性相同

b、如果x與根節點奇偶性相同,y與根節點奇偶性不同,那麼x,y奇偶性不同

c、如果x與根節點奇偶性不同,y與根節點奇偶性相同,那麼x,y奇偶性不同

d、如果x與根節點奇偶性不同,y與根節點奇偶性不同,那麼x,y奇偶性相同

所以,對於條件:l r even來說,

設x=l-1,y=r,x的父節點為px,y的父節點為py

可以首先判斷x和y是否在同一個集合

如果在同一個集合

  則判斷d[l-1]與d[r]之間的關係是否表示奇偶性相同,奇偶性相同意味著d[l-1]==d[r],如果不相等,說明產生矛盾

如果不在同一個集合

  則進行集合合併,設將px合併到py上,為了維護元素之間的奇偶性關係,還要更新d[px],如下圖,由於x,y奇偶性相同,那麼x到y的虛擬權值是0,x到px的權值之和d[x]+d[px]等於0+d[py],因此d[px] = (d[py] - d[x] + 2) % 2,+2再%2是為了避免負數。

洛谷題單指南-字首和差分與離散化-P5937 [CEOI1999] Parity Game

同理,對於條件:l r odd來說,

設x=l-1,y=r,x的父節點為px,y的父節點為py

可以首先判斷x和y是否在同一個集合

如果在同一個集合

  則判斷d[l-1]與d[r]之間的關係是否表示奇偶性不同,奇偶性不同意味著d[l-1]!=d[r],如果相等,說明產生矛盾

如果不在同一個集合

  則進行集合合併,設將px合併到py上,為了維護元素之間的奇偶性關係,還要更新d[px],如下圖,由於x,y奇偶性相同,那麼x到y的虛擬權值是1,x到px的權值之和d[x]+d[px]等於1+d[py],因此d[px] = (1 + d[py] - d[x] ) % 2。

洛谷題單指南-字首和差分與離散化-P5937 [CEOI1999] Parity Game

100分程式碼:

#include <bits/stdc++.h>
using namespace std;

const int N = 5005;
int n, m;
struct node
{
    int l, r;
    string type;
} a[N];
int s[N * 2], cnt; //用於離散化的陣列
map<int, int> h; //用於離散化的map
int p[N * 2], d[N * 2]; //p:並查集 d:每個元素到父節點的權值%2

int find(int x)
{
    if(p[x] != x)
    {
        int tmp = find(p[x]);
        d[x] = (d[x] + d[p[x]]) % 2;
        p[x] = tmp;
    }
    return p[x];
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++) 
    {
        cin >> a[i].l >> a[i].r >> a[i].type;
        s[++cnt] = a[i].l;
        s[++cnt] = a[i].r;
    }

    //離散化
    int idx = 0;
    sort(s + 1, s + cnt + 1);
    for(int i = 1; i <= cnt; i++)
    {
        if(!h.count(s[i])) h[s[i]] = ++idx;
    }
    for(int i = 1; i <= m; i++)
    {
        a[i].l = h[a[i].l];
        a[i].r = h[a[i].r];
    }

    //並查集初始化
    for(int i = 1; i <= cnt; i++) p[i] = i;

    int ans = m;
    //處理
    for(int i = 1; i <= m; i++)
    {
        int x = a[i].l - 1, px = find(x);
        int y = a[i].r, py = find(y);
        if(a[i].type == "even") //x、y同奇、偶
        {
            if(px == py) //已經在同一個集合
            {
                if(d[x] != d[y]) //x,y到根節點權值不相等,表示奇、偶不同,產生矛盾
                {
                    ans = i - 1;
                    break;
                }
            }
            else //合併x、y
            {
                p[px] = py;
                d[px] = (d[y] - d[x] + 2) % 2;
            }
        }
        else if(a[i].type == "odd") //x、y不同奇、偶
        {
            if(px == py)
            {
                if(d[x] == d[y]) //x,y到根節點權值相等,表示奇、偶相同,產生矛盾
                {
                    ans = i - 1;
                    break;
                }
            }
            else
            {
                p[px] = py;
                d[px] = (1 + d[y] - d[x]) % 2;
            }
        }
    }
    cout << ans;
    return 0;
}

3、擴充套件域並查集

帶權並查集的本質是透過節點與根節點的權值關係來判定節點之間的關係,如果不夠直觀,下面介紹一種更容易理解的方法:擴充套件域並查集,又叫做種類並查集。

由於要將每一個元素是偶數、奇數兩種情況都儲存下來,需要將並查集長度進行擴充套件,此題元素有兩種狀態,只需要擴充套件2倍長度。

對於一個元素x,可以定義兩個條件:x是偶數,x+cnt是奇數,

對於一個元素y,也可定義兩個條件:y是偶數,y+cnt是奇數,

這樣,

在遇到一組判斷l r even時,令x=l-1,y=r,如果find(x)==find(y+cnt)則表示有矛盾,然後將"x是偶數 y是偶數"加入集合,將"x+cnt是奇數,y+cnt是奇數"加入集合

在遇到一組判斷l r odd時,令x=l-1,y=r,如果find(x)==find(y)則表示有矛盾,然後將"x是偶數 y+cnt是奇數"加入集合,將"x+cnt是奇數 y是偶數"加入集合

100分程式碼:

#include <bits/stdc++.h>
using namespace std;

const int N = 5005;
int n, m;
struct node
{
    int l, r;
    string type;
} a[N];
int s[N * 2], cnt; //用於離散化的陣列
map<int, int> h; //用於離散化的map
int p[N * 4]; //p:並查集,擴充套件兩倍長度

int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++) 
    {
        cin >> a[i].l >> a[i].r >> a[i].type;
        s[++cnt] = a[i].l;
        s[++cnt] = a[i].r;
    }

    //離散化
    int idx = 0;
    sort(s + 1, s + cnt + 1);
    for(int i = 1; i <= cnt; i++)
    {
        if(!h.count(s[i])) h[s[i]] = ++idx;
    }
    for(int i = 1; i <= m; i++)
    {
        a[i].l = h[a[i].l];
        a[i].r = h[a[i].r];
    }

    //並查集初始化
    for(int i = 1; i <= 2 * cnt; i++) p[i] = i;

    int ans = m;
    //處理
    for(int i = 1; i <= m; i++)
    {
        int x = a[i].l - 1, y = a[i].r;
        if(a[i].type == "even") //x、y同奇、偶
        {
            if(find(x) == find(y + cnt))
            {
                ans = i - 1;
                break;
            }
            p[find(x)]  = find(y);
            p[find(x + cnt)] = find(y + cnt);
        }
        else if(a[i].type == "odd") //x、y不同奇、偶
        {
            if(find(x) == find(y))
            {
                ans = i - 1;
                break; 
            }
            p[find(x)] = find(y + cnt);
            p[find(x + cnt)] = find(y);
        }
    }
    cout << ans;
    return 0;
}

相關文章