この問題かなり難しく感じました.
よく観察すると典型に落とし込めるということで,記事にしました.
問題概要
文字列 $ S = S_1S_2 \cdots S_{|S|} $ のコストを次のように定義する.
- $ 1 \leq i < j \leq |S| $ のようなペア $ (i,j) $ であって, $ S_i = S_j $ かつ $ S _ {i+1}=S _ {j+1} $ であるようなものの数
英小文字の最初の $ K $ 文字のみで構成される長さ $ N $ の文字列でコストが最小のものを $ 1 $ つ出力せよ.
制約
- $ 1 \leq N \leq 2 \times 10^{5} $
- $ 1 \leq K \leq 26 $
考察
まず,長さ $ N $ 文字列には $ 2 $ 文字の連続する部分文字列が(重複を含めて) $ N-1 $ 個ある.
文字列のコストはこの $ N-1 $ 個の部分列の等しいものの組み合わせの総和と等しい.
よってコストを $ cost $ ,連続する部分文字列 $ ij $ の数を $ cnt_{i,j} $ として,次の式が成り立つ.
\begin{align}
cost &= \sum _ {i=\rm{'a'}} ^ {\rm{'z'}} \sum _ {j=\rm{'a'}} ^ {\rm{'z'}} \frac {(cnt _ {i,j})(cnt _ {i,j}-1)}{2} \\
&= \frac{1}{2} \left\{ \sum _ {i=\rm{'a'}} ^ {\rm{'z'}} \sum _ {j=\rm{'a'}} ^ {\rm{'z'}} (cnt _ {i,j})^{2} - \sum _ {i=\rm{'a'}} ^ {\rm{'z'}} \sum _ {j=\rm{'a'}} ^ {\rm{'z'}} cnt _ {i,j} \right\} \\
&= \frac{1}{2} \left\{ \sum _ {i=\rm{'a'}} ^ {\rm{'z'}} \sum _ {j=\rm{'a'}} ^ {\rm{'z'}} (cnt _ {i,j})^{2} - (N-1) \right\} \hspace{15pt} \left(\because \sum _ {i=\rm{'a'}} ^ {\rm{'z'}} \sum _ {j=\rm{'a'}} ^ {\rm{'z'}} cnt _ {i,j} = N-1\right)
\end{align}
上の式で変数は $ cnt_{i,j} $ のみであるから,$ cost $ を最小化するには $ \sum _ {i=\rm{'a'}} ^ {\rm{'z'}} \sum _ {j=\rm{'a'}} ^ {\rm{'z'}} (cnt _ {i,j})^{2} $ を最小化すればよいことがわかる.
$ \sum _ {i=\rm{'a'}} ^ {\rm{'z'}} \sum _ {j=\rm{'a'}} ^ {\rm{'z'}} cnt _ {i,j} = N-1 $ であるから,和が一定の数列の二乗和を最小化すればよいことが分かり,これは(できるだけ)等しくなるように分配すれば良い(証明略).
※実数の場合はコーシー・シュワルツの不等式で簡単に証明できる.
したがって,なるべく $ 2 $ 文字の連続部分文字列が被らないように文字列を構築していくことを考えればよい.この連続部分文字列は $ K^{2} $ 種類存在する.そして実は $ K^{2} $ 種類の $ 2 $ 文字連続部分文字列を全て含む 長さ $ K^{2}+1 $ の文字列を構成することが可能である.
ここでアルファベットを頂点に対応させたグラフを考える.
すると $ 2 $ 文字の連続部分文字列は辺に対応し,文字列はこのグラフ上の歩道に対応する.
任意の辺を 1 回ずつ通る歩道をグルグル回れば,連続部分文字列の分布を最もフラットにできる.
このような歩道は,オイラー路(Eulerian trail)の定義そのものである.
オイラー路を持つためには,すべての頂点の入次数と出次数が等しくなければならない.
このグラフは,すべての頂点間に 1 本ずつ有効辺が伸びているため明らかに条件を満たしている.
オイラー路の構築はグラフのサイズを $ M $ として, DFS で $ O(M) $ で構築が可能である(隣接行列のためのメモリの確保は別に考えています).
オイラー路に対応する文字列を構築したら,長さ $ N $ に達するまで連結することで答えが得られる.
全体で $ O(N+K^{2}) $ で答えを得ることができる.
別解(コンテスト中の解法)
コンテスト中には全然オイラー路が見えなかったが長さ $ 2 $ の連続部分文字列 $ K^{2} $ 種類が被らない文字列を規則的に構築するための観察をしていた.
- $ K=1 $ の時は自明に "aa"
- $ K \geq 2 $ の時は $ K - 1 $ の時の文字列に $ K $ 番目のアルファベットを含む $ 2 $ 文字の文字列を含むようにすれば良い.
- $ K=2 $ の時 "bb"+"aa"+"b"
- $ K=3 $ の時 "cc"+"bbaab"+"cac"
上記のように再帰的に構築可能である.
個人的ポイント
- 要素を頂点,ペアを辺に対応させてグラフの問題に落とすことは多々ある.
- オイラー路は $ O(M) $ で構成可能.
- 厳密には隣接行列を連想配列でするなどにより log が付いたりしそう.
- $ 1 $ パラメータの構築は,$ 1 $ つ前のものから簡単な手続きで作れることが多い.
参考資料
競プロにおけるオイラー路とその応用について - Learning Algorithms
オイラー路解
再帰構築解
▶ソースコードを展開
#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(int)(n);i++)
#define FOR(i,n,m) for(int i=(int)(n); i<=(int)(m); i++)
#define RFOR(i,n,m) for(int i=(int)(n); i>=(int)(m); i--)
#define ITR(x,c) for(__typeof(c.begin()) x=c.begin();x!=c.end();x++)
#define RITR(x,c) for(__typeof(c.rbegin()) x=c.rbegin();x!=c.rend();x++)
#define setp(n) fixed << setprecision(n)
template<class T> bool chmax(T &a, const T &b) { if (a<b) { a=b; return 1; } return 0; }
template<class T> bool chmin(T &a, const T &b) { if (a>b) { a=b; return 1; } return 0; }
#define ll long long
#define vll vector<ll>
#define vi vector<int>
#define pll pair<ll,ll>
#define pi pair<int,int>
#define all(a) (a.begin()),(a.end())
#define rall(a) (a.rbegin()),(a.rend())
#define fi first
#define se second
#define pb push_back
#define ins insert
#define debug(a) cerr<<(a)<<endl
#define dbrep(a,n) rep(_i,n) cerr<<(a[_i])<<" "; cerr<<endl
#define dbrep2(a,n,m) rep(_i,n){rep(_j,m) cerr<<(a[_i][_j])<<" "; cerr<<endl;}
using namespace std;
template<class A, class B>
ostream &operator<<(ostream &os, const pair<A,B> &p){return os<<"("<<p.fi<<","<<p.se<<")";}
template<class A, class B>
istream &operator>>(istream &is, pair<A,B> &p){return is>>p.fi>>p.se;}
template<class T>
vector<T> make_vec(size_t a){
return vector<T>(a);
}
template<class T, class... Ts>
auto make_vec(size_t a, Ts... ts){
return vector<decltype(make_vec<T>(ts...))>(a, make_vec<T>(ts...));
}
int main(void)
{
cin.tie(0);
ios::sync_with_stdio(false);
int N,K; cin>>N>>K;
string S="aa";
rep(i,K-1){
char ch=(char)(i+'a'+1);
string pref=""; pref+=ch; pref+=ch;
string suff="";
rep(j,i*2+1){
if (j&1){
suff+=(char)('a'+j/2);
}else{
suff+=ch;
}
}
S=pref+S+suff;
}
while(S.size()<N) S+=S.substr(1);
cout<<S.substr(0,N)<<"\n";
return 0;
}