kmjp's blog

競技プログラミング参加記です

yukicoder : No.1669 パズル作成

問題を読み間違えて悩んだ。
https://yukicoder.me/problems/no/1669

問題

N*Nのグリッドがあり、うちMマスだけ黒いマスが指定される。
それ以外のマスは白である。

以下の手続きを取り、全マスを黒くしたい。

  • まず、いくつかの白マスを選び黒くする。
  • その後、以下の処理を任意回数繰り返す
    • 列または行を選び、その該当するセルを白黒反転する

条件を満たすために、最初に選ぶ白マスは最小何個か。

解法

最初の処理を行った後のことを考える。
反転は同一列・行で2回以上行う意味はないので、1回までしか行わないとする。
黒マスのあるセルについては、列・行の反転回数のが一致しなければならない。
反対に、白マスのあるセルについては、列・行の反転回数のが不一致でなければならない。

列に対応するN点と、行に対応するN点の計2点からなる2部グラフを考える。
黒マスに相当する列と行の点の間に辺を張ることを考える。
辺を結んだ列・行は反転回数が一致していないといけないので、連結成分内は完全二部グラフになるよう辺を追加しなければならない。
言い換えると、連結成分内で初期状態で辺が張られていない列・行の点の間は、最初の白マス選択が必要である。

列のうち反転回数が1のものがa個、行のうち反転回数が1のものがb個あるとすると、張られるべき辺はab+(N-a)*(N-b)本となり、うちM本はすでに辺があるので、追加分はab+(N-a)*(N-b)-Mとなる。
さて、ここで各列・辺に反転回数0/1のどちらを割り当てるか考える。
ただし、連結成分については列・行全体を0または1で同じ値を割り当てなければいけない。

普通に考えると、いくつか連結成分を処理した後、a,bの状態を考えると状態がO(N^2)通りで、連結成分の数だけ処理するとO(N^3)かかる。
ただ実際にはa,bを全通り列挙する必要はない。
求めたいのはab+(N-a)*(N-b)-Mの最小値なので、aごとにbの最小値か最大値だけ求めておけばよく、O(N^2)で済む。

template<int um> class UF {
	public:
	vector<int> par,rank,cnt;
	UF() {par=rank=vector<int>(um,0); cnt=vector<int>(um,1); for(int i=0;i<um;i++) par[i]=i;}
	void reinit(int num=um) {int i; FOR(i,num) rank[i]=0,cnt[i]=1,par[i]=i;}
	int operator[](int x) {return (par[x]==x)?(x):(par[x] = operator[](par[x]));}
	int count(int x) { return cnt[operator[](x)];}
	int operator()(int x,int y) {
		if((x=operator[](x))==(y=operator[](y))) return x;
		cnt[y]=cnt[x]=cnt[x]+cnt[y];
		if(rank[x]>rank[y]) return par[x]=y;
		rank[x]+=rank[x]==rank[y]; return par[y]=x;
	}
};
UF<10101> uf;
int N,M;
int A[10101],B[10101];
int ma[5050],mi[5050];
void solve() {
	int i,j,k,l,r,x,y; string s;
	
	cin>>N>>M;
	FOR(i,M) {
		cin>>x>>y;
		uf(x-1,N+y-1);
	}
	FOR(i,N) A[uf[i]]++;
	FOR(i,N) B[uf[N+i]]++;
	int SA=0,SB=0;
	FOR(i,2*N) if(A[i]+B[i]==1) {
		if(A[i]==1) SA++;
		if(B[i]==1) SB++;
	}
	FOR(i,N+1) ma[i]=-1<<30,mi[i]=1<<30;
	FOR(i,SA+1) ma[i]=SB,mi[i]=0;
	FOR(i,2*N) if(A[i]+B[i]>1) {
		for(j=N;j>=0;j--) if(ma[j]>=0) {
			ma[j+A[i]]=max(ma[j+A[i]],ma[j]+B[i]);
			mi[j+A[i]]=min(mi[j+A[i]],mi[j]+B[i]);
		}
	}
	int ret=1LL<<30;
	FOR(i,N+1) if(ma[i]>=0) {
		ret=min(ret,i*ma[i]+(N-i)*(N-ma[i])-M);
		ret=min(ret,i*mi[i]+(N-i)*(N-mi[i])-M);
	}
	cout<<ret<<endl;
	
}

まとめ

白マスを黒マスにする操作も、任意タイミングで行えると勘違いしてしまい、解説を見て最初混乱した。