5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

なんかキャンペーンの奴

B問題の「みんなでしりとり」を解いてみました。

準備

入力用関数として以下のものを用意しています。

// 1行文字列
func readStr() -> String {
  readLine()!
}

// 1行整数
func readInt() -> Int {
  Int(readLine()!)!
}

// 空白文字で分割された文字列
func readSpStrs() -> [Substring] {
  readLine()!.split(separator: " ")
}

// 空白文字で分割された整数
func readSpInts() -> [Int] {
  readSpStrs().map { Int($0)! }
}

// 空白文字で分割された2つの整数をタプルで
func readInt2() -> (Int, Int) {
  let ints = readSpInts()
  return (ints[0], ints[1])
}

// 空白文字で分割された3つの整数をタプルで
func readInt3() -> (Int, Int, Int) {
  let ints = readSpInts()
  return (ints[0], ints[1], ints[2])
}

// N行分の整数
func readInt(_ n: Int) -> [Int] {
  (1...n).map { _ in readInt() }
}

解説?

ルール

問題はあらかじめ用意されたリストの単語を使ってしりとりを行い、最後に残った人数とその番号を表示するというものです。
「ん」の代わりに末尾が「z」の単語はアウトになります。
また一度使った単語は使えません。使った場合はアウトです。

単語のリスト

今回の場合、間に合ってしまうのですが、単語の数やログの数が多い場合はArrayを使うとTLEになると思いますので、こういったリストはSetにしておくといいでしょう。
今回は間に合うのでArrayのままです。

読み込みは以下の通り。使用した単語を削除していくためvarで宣言してます。

// 単語リストを読み込み。
var wordList = (0..<k).map { _ in readStr() }

しりとりの判定

なんだかよくわからない実装です。

// しりとり判定(アウトならfalse)の関数を返す。
func getJudgeSystem() -> (String) -> Bool {
  
  // ひとつ前の単語の末尾をストア。新たに開始した場合はnil
  var prevEnd: Character?
  
  func judge(_ word: String) -> Bool {
    // しりとり判定
    // OKなら次の頭文字をNGならnilを返す
    func nextHead() -> Character? {
      // 単語リストに存在するかをチェック
      guard let wordIndex = wordList.firstIndex(of: word) else { return nil }
      
      // 単語を使用済みにする(単語リストから削除する)
      wordList.remove(at: wordIndex)
      
      // zで終わるかチェック
      guard let lw = word.last, lw != "z" else { return nil }
    
      // 新たに開始した場合はOK
      if prevEnd == nil { return lw }
    
      // しりとりになっているかチェック
      return prevEnd == word.first ? lw : nil
    }
    
    prevEnd = nextHead()
    
    return prevEnd != nil
  }
  
  return judge
}

しりとりを判定するjudge関数を作りたいのですが、しりとりですので前の単語そのものかその末尾を記録しておく必要があります。
これにクロージャと変数のキャプチャを用いています。
クロージャである内部関数judgeが前の単語の末尾を記録した変数prevEndをキャプチャすることでしりとりが成立しているかを判定しています。
さらにその内部関数であるnextHead関数はしりとりが成立していれば次の頭文字を、成立していなければnilを返します。
最終的にprevEndnilであればしりとりは失敗です。

ログをチェック

最後にログをチェックして勝ち残りを出力します。
すでに変数sにログを読み込んでいます。

// 生存者リスト。ここに最後まで残ったものが回答
var survivor = Array(1...n)

// 現在のプレーヤーの↑の配列でのインデックス
var currentPlayerIndex = 0

let judge = getJudgeSystem()
for word in s {
  if judge(word) {
    // プレーヤーインデックスを進ませる
    currentPlayerIndex = (currentPlayerIndex + 1) % survivor.count
    continue
  }
  
  // プレーヤーを除外
  survivor.remove(at: currentPlayerIndex)
  
  // プレーヤーインデックスを進ませる
  // 現在のプレーヤーが削除されたので次のプレーヤーインデックスは変わらない
  // ただし、削除対象が配列内で一番最後だった場合0になるように再計算
  currentPlayerIndex = currentPlayerIndex % survivor.count
}

print(survivor.count)
survivor.forEach { print($0) }

ここは特に解説は不要でしょう。

全プログラムリスト

// 1行文字列
func readStr() -> String {
  readLine()!
}

// 1行整数
func readInt() -> Int {
  Int(readLine()!)!
}

// 空白文字で分割された文字列
func readSpStrs() -> [Substring] {
  readLine()!.split(separator: " ")
}

// 空白文字で分割された整数
func readSpInts() -> [Int] {
  readSpStrs().map { Int($0)! }
}

// 空白文字で分割された2つの整数をタプルで
func readInt2() -> (Int, Int) {
  let ints = readSpInts()
  return (ints[0], ints[1])
}

// 空白文字で分割された3つの整数をタプルで
func readInt3() -> (Int, Int, Int) {
  let ints = readSpInts()
  return (ints[0], ints[1], ints[2])
}

// N行分の整数
func readInt(_ n: Int) -> [Int] {
  (1...n).map { _ in readInt() }
}


/** ************************************
  ここから
************************************ **/

let (n, k, m) = readInt3()

// 単語リストを読み込み。
var wordList = (0..<k).map { _ in readStr() }

// ログ読み込み
let s = (0..<m).map { _ in readStr() }

// しりとり判定 アウトならfalse
func getJudgeSystem() -> (String) -> Bool {
  
  // ひとつ前の単語の末尾をストア。新たに開始した場合はnil
  var prevEnd: Character?
  
  func judge(_ word: String) -> Bool {
    // しりとり判定
    // OKなら次の頭文字をNGならnilを返す
    func nextHead() -> Character? {
      // 単語リストに存在するかをチェック
      guard let wordIndex = wordList.firstIndex(of: word) else { return nil }
      
      // 単語を使用済みにする(単語リストから削除する)
      wordList.remove(at: wordIndex)
      
      // zで終わるかチェック
      guard let lw = word.last, lw != "z" else { return nil }
    
      // 新たに開始した場合はOK
      if prevEnd == nil { return lw }
    
      // しりとりになっているかチェック
      return prevEnd == word.first ? lw : nil
    }
    
    prevEnd = nextHead()
    
    return prevEnd != nil
  }
  
  return judge
}


// 生存者リスト。ここに最後まで残ったものが回答
var survivor = Array(1...n)

// 現在のプレーヤーの↑の配列でのインデックス
var currentPlayerIndex = 0

let judge = getJudgeSystem()
for word in s {
  if judge(word) {
    // プレーヤーインデックスを進ませる
    currentPlayerIndex = (currentPlayerIndex + 1) % survivor.count
    continue
  }
  
  // プレーヤーを除外
  survivor.remove(at: currentPlayerIndex)
  
  // プレーヤーインデックスを進ませる
  // 現在のプレーヤーが削除されたので次のプレーヤーインデックスは変わらない
  // ただし、削除対象が配列内で一番最後だった場合0になるように再計算
  currentPlayerIndex = currentPlayerIndex % survivor.count
}

print(survivor.count)
survivor.forEach { print($0) }

まとめ

内部関数はクロージャです!!
Swift界隈ではクロージャ式のみをクロージャと呼ぶ傾向がありますが、クロージャの本質は変数のキャプチャにあります。
通常の内部関数もクロージャです!!
クロージャ式だけがクロージャでないことに注意しましょう。
グローバルスコープの関数だってクロージャです!!

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?