Problem:
D: しりとり圧縮 (Shiritori Compression)

問題

えびちゃんとかにちゃんの二人はしりとりをしました。えびちゃんはしりとりで出た単語のメモを見ています。

えびちゃんは、この単語列 w_1, w_2, ..., w_N から冗長な連続部分列を、存在しなくなるまで取り除きたいと思っています。えびちゃんの思う冗長な連続部分列とは以下のように定義されます。

以下を満たす i, j が存在したとき、連続部分列 w_i, ..., w_{j-1} は冗長です。

i < j なる添字 i, j について、単語 w_i と w_j の先頭の文字が等しい。

このとき、えびちゃんは添字 i 以上 j-1 以下の単語を取り除きます。

たとえば、次のような単語列を考えます。

apple→editor→random→me→edge

これは、次のように圧縮されます。

apple→edge

editor と edge の先頭の文字が e で一致しているため、editor から edge の直前（me）までの単語が取り除かれます。

えびちゃんの判断基準では、末尾の文字が同じであっても冗長とは見なさないことに注意してください。たとえば、上の圧縮済み単語列において、apple と edge の末尾の文字はどちらも e ですが、このことによって単語が取り除かれることはありません。

また、以下のような例も考えられます。

orange→eel→luck

banana→at→tomb→bus→sound→does→some

peach→hero→owl→loop→proof→fish→he

これらはそれぞれ次のように圧縮できます。いずれも、最後の単語は保存されることに注意してください。

orange→eel→luck

すでに圧縮済みです。

bus→some

banana から bus、sound から some がそれぞれ圧縮可能です。

peach→he

proof→fish→he と圧縮することもできますが、圧縮後の単語数が異なっていることに注意してください。

えびちゃんのメモが与えられるので、それらを圧縮して得られる単語列の長さ（単語数）の最小値 L を出力してください。

なお、「同じ単語が複数回現れる」または「ある単語の先頭の文字が直前の単語の末尾の文字と一致していない」ことは起こらないと仮定して構いません。

入力形式

N
w_1
...
w_N

一行目に単語数 N が与えられ、1+i 行目には i 番目の単語が与えられます。

制約

1\leq N\leq 10^5

1\leq $\sum_{i=1}^N$|w_i| \leq 10^6

w_i の各文字は英小文字

出力形式

L を一行に出力してください。それ以外の余計な文字は含まれてはいけません。

入力例1

7
banana
at
tomb
bus
sound
does
some

出力例1

2

上で挙げた例です。

入力例2

7
peach
hero
owl
loop
proof
fish
he

出力例2

2

上で挙げた別の例です。

Solution in Rust (judge verdict: Accepted)

/// Thank you tanakh!!!
///  https://qiita.com/tanakh/items/0ba42c7ca36cd29d0ac8
macro_rules! input {
    (source = $s:expr, $($r:tt)*) => {
        let mut iter = $s.split_whitespace();
        input_inner!{iter, $($r)*}
    };
    ($($r:tt)*) => {
        let mut s = {
            use std::io::Read;
            let mut s = String::new();
            std::io::stdin().read_to_string(&mut s).unwrap();
            s
        };
        let mut iter = s.split_whitespace();
        input_inner!{iter, $($r)*}
    };
}

macro_rules! input_inner {
    ($iter:expr) => {};
    ($iter:expr, ) => {};

    ($iter:expr, $var:ident : $t:tt $($r:tt)*) => {
        let $var = read_value!($iter, $t);
        input_inner!{$iter $($r)*}
    };
}

macro_rules! read_value {
    ($iter:expr, ( $($t:tt),* )) => {
        ( $(read_value!($iter, $t)),* )
    };

    ($iter:expr, [ $t:tt ; $len:expr ]) => {
        (0..$len).map(|_| read_value!($iter, $t)).collect::<Vec<_>>()
    };

    ($iter:expr, chars) => {
        read_value!($iter, String).chars().collect::<Vec<char>>()
    };

    ($iter:expr, usize1) => {
        read_value!($iter, usize) - 1
    };

    ($iter:expr, $t:ty) => {
        $iter.next().unwrap().parse::<$t>().expect("Parse error")
    };
}

use std::cmp;

const INF: usize = 1 << 30;

fn main() {
    input!(n: usize, s: [chars; n]);
    let mut dictionary = vec![vec![]; 26];
    for i in 0..n {
        let c = s[i][0] as usize - 'a' as usize;
        dictionary[c].push(i);
    }

    let mut jump = vec![INF; n];
    for i in 0..26 {
        for j in 1..dictionary[i].len() {
            jump[dictionary[i][j - 1]] = dictionary[i][j];
        }
    }

    let mut dp = vec![INF; n + 1];
    dp[0] = 0;
    for i in 0..n {
        dp[i + 1] = cmp::min(dp[i + 1], dp[i] + 1);
        if jump[i] != INF {
            dp[jump[i]] = cmp::min(dp[jump[i]], dp[i]);
        }
    }

    println!("{}", dp[n]);
}