アジの開きを閉じる。

競プロ(AtCoder)中心のブログ

ABC304 F - Shift Table

執筆時レート:水色

問題文→https://atcoder.jp/contests/abc304/tasks/abc304_f
公式解説→https://atcoder.jp/contests/abc304/editorial/6511

目次

問題

高橋君と青木君は  N 日間アルバイトをする.
高橋君のシフト表は文字列  S で与えられ, S [i] #ならば  i 日目に出勤,. ならば  i 日目に欠勤する.
一方,青木君は以下の手順でシフト表を作成する.

  •  N でない  N の正の約数  M をとる.
  •  1 日目から  M 日目までの勤怠を決める.
  •  i = 1, 2,...,N-M の順に  M+i 日目の勤怠が  i 日目の勤怠と一致するように  M+i 日目の勤怠を決める.

 M の値が異なる場合でも最終的なシフトが等しくなる場合があることに注意.
 N 日すべてについて高橋君と青木君の少なくとも一方が出勤することになったとき,青木君のシフト表として考えられるものの個数を  998244353 で割ったあまりを求めよ.

制約

  •  N  2 以上  {10}^{5} 以下の整数
  •  S は長さ  N #, .からなる文字列

方針

サンプルケース1で考えてみる.

  •  N=6
  •  S= ##.#.#

青木君のシフト表を  T とする.
 M=3 で考えてみる.
高橋君が欠勤する日は青木君は必ず出勤しなければならないので,最初の  M 日間のうち何日かは出勤で確定する(?は出勤か欠勤か選べる).

  •  T[:M]=?##

よって  M=3 の場合では  {2}^{?の個数}  2 通り!
同じように他の  M でも求めて合計すればOK!
としたいところだが,?#にしたときの  T= ######は,
 M=1,2 でも生成でき,重複して計上してしまう.

問題は重複をどう排除するか?になる.

このようなとき,(多分)2通りの考え方がある.

  1. パターンと1対1対応するものを数える
  2. とりあえず重複を区別して数えてからダブりを引く

1.は今回だとTの最小周期の文字列に対応させるという考え方.
例えば  T= ######の最小周期は#なのでこれだけを直接数えるように頑張る.
でもムズカシイ.

2.は, M を全探索してどこかのタイミングでダブりを引く.
 M を固定したとき,重複を区別すると  {2}^{?の個数} というのは簡単.
どのようにしてダブりを引くか?

######を計上するのは1.と同じように最小周期で数えるとして, M がそれ以外の周期のときでは計上しないようにしたい.

 M=3 と固定して考える.
.##.##のような,「 M が最小周期である場合」はここで初めて計上する.
一方,######のような,「 M が最小周期でない場合」は, M が最小周期のときに計上してるはずなので,ここでは計上しないようにする.

では「 M が最小周期でない場合」を引くために,最小周期と  M の関係について調べてみる.

最小周期を  t ( \lt M ) とすると弱周期性補題より  \gcd (t,M) も周期.
 \gcd (t,M) \leq t であり, \gcd (t,M) \lt t と仮定すると  t が最小周期であることに矛盾するので, \gcd (t,M) = t . よって  t  M の約数である.

つまり,「最小周期  \Rightarrow M でない  M の約数」が成立する.

よって,各  M では「 M が最小周期である場合」を計上しているので,ある  M が最小周期である場合の数は,

  •  \displaystyle {2}^{?の個数} - \sum_{t \in {\{MでないMの約数\}}}{tが最小周期である場合の数}

で求められる.  M が小さい方から全探索すればよい.

計算量

 N の約数の個数を  d(N) とする.
 M について,?の個数を数えるのに  O(N) ,ダブりを引くのに  O(M) かかるので,結局  O(N) かかる.
よって,最終的な計算量は  O(Nd(N)) となる.
今回の制約(  N \leq {10}^{5} )では,Nの約数の個数は高々128個なので十分高速である.

実装

https://atcoder.jp/contests/abc304/submissions/41989568

#include <bits/stdc++.h>
using namespace std;
#define rep(i,n) for(int i=0; i<n; i++)

/* Modint構造体(省略) */

// 約数列挙
vector<ll> divisor(ll n){
  vector<ll> ret;
  for(ll i=1;i*i<=n;i++){
    if(n%i==0) {
      ret.push_back(i);
      if(i!=n/i) ret.push_back(n/i);
    }
  }
  sort(ret.begin(),ret.end());
  return ret;
}
 
int main(){
  int n;cin>>n;
  string s;cin>>s;
 
  // nの約数列挙
  auto divs=divisor(n);
 
  // rem[i]:=最小周期がiである場合の数
  vector<mint> rem(n+1);
  // p2[i]:=2のi乗
  vector<mint> p2(n+1);
  p2[0]=1;
  rep(i,n) p2[i+1]=p2[i]*2;
  
  for(auto m:divs){
    if(m==n) continue;
 
    // 出勤/欠勤を選べる日数を数える
    int cnt=0;
    vector<bool> must(m);
    rep(i,n) {
      if(s[i]=='.'){
        must[i%m]=true;
      }
    }
    rep(i,m) if(!must[i]) cnt++;
 
    // 重複を区別した場合の数
    rem[m]=p2[cnt];
 
    // ここからダブりを引く
    for(int i=1;i<m;i++){
      if(m%i==0){
        rem[m]-=rem[i];
      }
    }
 
  }
 
  // remの合計が答え
  mint res=0;
  for(auto r:rem) res+=r;
 
  cout<< res <<'\n';
 
  return 0;
}