Breaking Into Atoms (ATOMS)

keyword

集合 C++

問題概要

0~N-1(N<100)を要素に持つ集合がM(<30)個ある。全ての集合を部分集合族A_iで被覆する。A_iの要素は全てS_jに含まれるか全てS_jに含まれていないかのどちらかである。最小いくつで被覆できるか求める問題。

解法

0~N-1から2つ選んで同じかどうかUnion Findで管理する。計算量O(N^2 * M) (実際の実装はsetで手抜きしている分log Nがかかる)。各要素がどのS_jに含まれるかで管理する方法もあって、これだと計算量O(N*M)。

int pars[102];
bool visited[102];
int getRoot(int x){ return x==pars[x]?x:(pars[x]=getRoot(pars[x])); }
bool isSame(int x,int y){ return getRoot(x)==getRoot(y); }
void merge(int x,int y){ pars[getRoot(x)] = getRoot(y); }

set<int> S[33];
int N, M;

int solve(){
    for(int i=0; i<N; i++) pars[i]=i;
    for(int i=0; i<N; i++) visited[i] = false;
    for(int i=0; i<M; i++){
        EACH(S[i],it) visited[*it] = true;
    }
    for(int i=0; i<N; i++){
        for(int j=i+1; j<N; j++){
            bool same = true;
            for(int k=0;k<M;k++){
                if((S[k].find(i)==S[k].end()) ^ (S[k].find(j)==S[k].end())) same=false;
            }
            if(same) merge(i,j);
        }
    }
    int ans = 0;
    for(int i=0; i<N; i++)if(visited[i]){
        if(getRoot(i)==i) ans++;
    }
    return ans;
}

int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&N,&M);
        for(int i=0; i<M; i++)S[i].clear();
        for(int i=0; i<M; i++){
            int k;
            scanf("%d",&k);
            for(int j=0; j<k; j++){
                int x;
                scanf("%d",&x);
                S[i].insert(x);
            }
        }
        printf("%d\n",solve());
    }
    return 0;
}