SRM 570 Div I

SRM 570 Div I

http://community.topcoder.com/stat?c=round_overview&er=5&rd=15490

250に37分。チャレンジされ撃墜。レートは1474から1335へ。Room 21

Problem Status Points
250 RobotHerb Challenge Succeeded 0.00
500 CentaurCompany Opened 0.00
1000 CurvyonRails Unopened
  • 250に37分
    • チャレンジされ撃墜
    • 等号が足らず、境界条件に引っかかった
  • 目標が不達成に終わった。悔しい
  • 次回のSRMもしつこく「Div I Easyを奇麗な実装で提出する」を目標とする

RobotHerb

http://community.topcoder.com/stat?c=problem_statement&pm=12427&rd=15490&rm=315874&cr=22827765

37分。チャレンジされ撃墜された。

  • 制約から、Tが[1, 109]であった。愚直な実装だとTLEするように思えた
  • 一先ず愚直な実装を行いアリーナで計測。TLEすることを確認
  • Tが4の倍数である場合は計算を圧縮できる。実装を変更し37分で提出した
  • SRM後に失敗するケースを考えていたのだが思い浮かばず、@hotpepsi師匠にご教授いただいた

  • 実行した後に元の方向に戻るプログラム(a = {4}、a = {1, 3}等)を4回繰り返したときに誤作動を起こす実装なのであった
  • 師匠ありがとうございます

以下は修正した実装(システムテストをパス)。

class RobotHerb {
public:
  long long getdist(int T, std::vector<int> a) {
    long long dx, dy;
    int       dd;

    program(a, dx, dy, dd);

    long long x = 0, y = 0;
    int       d = dd;

    if (T >= 4) {
      simulate(dx, dy, dd, 4, x, y, d);

      x *= T / 4;
      y *= T / 4;
    }
    
    simulate(dx, dy, dd, T % 4, x, y, d);

    return std::abs(x) + std::abs(y);
  };

private:
  void program(const std::vector<int>& a, long long& x, long long& y, int& d) const {
    const long long dx[] = { 0, +1,  0, -1};
    const long long dy[] = {+1,  0, -1,  0};
    
    x = y = 0;
    d     = 0;
    
    for (int i = 0, size = a.size(); i < size; i ++) {
      x += dx[d] * a[i];
      y += dy[d] * a[i];
      
      d = (d + a[i]) % 4;
    }
  };

  void simulate(long long  dx, long long  dy, int dd, int T,
                long long&  x, long long&  y, int& d) const {
    
    for (int i = 0; i < T; i ++, d = (d + dd) % 4) {
      switch (d) {
      case 0:
        x += dx;
        y += dy;
        break;
      case 1:
        x += dy;
        y -= dx;
        break;
      case 2:
        x -= dx;
        y -= dy;
        break;
      case 3:
        x -= dy;
        y += dx;
        break;
      default:
        assert(false);
        break;
      }
    }
  }
};
  • @nico_shindanninさんの実装を参考にしながら初めから書き直す。実装がすっきりした

以下は書き直した実装(システムテストをパス)。

class RobotHerb {
public:
  long long getdist(int T, std::vector<int> a) {
    const long long dx[] = { 0, +1,  0, -1};
    const long long dy[] = {+1,  0, -1,  0};
    
    long long x = 0, y = 0;
    int       d = 0;

    int size = a.size();

    for (int t = 0; t < std::min(4, T); t ++)
      for (int i = 0; i < size; i ++) {
        x += dx[d] * a[i];
        y += dy[d] * a[i];
        
        d = (d  + a[i]) % 4;
      }

    if (T < 4)
      return std::abs(x) + std::abs(y);
  
    x *= T / 4;
    y *= T / 4;

    for (int t = 0; t < T % 4; t ++)
      for (int i = 0; i < size; i ++) {
        x += dx[d] * a[i];
        y += dy[d] * a[i];
        
        d = (d  + a[i]) % 4;
      }

    return std::abs(x) + std::abs(y);
  };
};
  • 周期性がある問題には苦手意識がある
  • 以前も境界条件(等号の見落とし)で落としたように思う
    • いかんなぁ
  • さて、SRM後のTLで@komaki__さんが興味深いツィートをしていた

  • なるほど、行列を使うことも出来るな...
  • ものすごく興味を惹かれたので考察してみた
  • 二次元平面を正規直交座標系であるとし、同次座標表現を使って考える。行列演算はPre-multipliedを基調にする
  • aiによる行列をMiとする。Miは移動と回転から構成されている。aiによる移動行列をTi、回転行列をRiとするとMiはこれらの行列を合成したものとして計算することができる


  • 配列aの要素数をnとする。M0からMn-1を合成した行列Mを計算したい。これは以下のように計算できる


  • 実質的に右辺は以下の行列の合成となる


  • 図示すると以下となる。aは{1, 4}とした。Mを差分として積み重ねて行くような感覚

  • 面白いことに任意の移動と回転を適用して出来た合成行列はある移動行列とある回転行列の合成行列として表すことができる


  • 行列の積は非可換なので、行列Tは行列T0、T1、...、Tn-1の合成行列ではない


  • 行列Rは行列R0、R1、...、Rn-1の合成行列と等しくなる


  • この等式は行列Mが移動行列と回転行列のみからなる行列であるため成り立っていることに注意が必要だ。結果が等しくなるというだけで行列の積の非可換性は変わらないし、一般的には成り立たない
  • 行列Tはもちろん「移動」を表す行列だ。しかしこれを「射影された原点の位置」を表す行列として捉えてみる。簡単にいえば「位置」だ
  • 行列Rは「回転」を表す行列であるが、同様に「射影された基底ベクトル」を表す行列として捉えてみよう。簡単にいえば「姿勢」だ
  • さて、移動行列を考えよう。二次元同次座標系での一般的な移動行列は以下だ。(tx, ty)を「位置」と捉えることができる


  • では回転行列を考えてみよう。以下は二次元同次座標系での一般的な回転行列だ


  • ちなみに回転行列は暗記しなくても軸上の2本のベクトル(1, 0)と(0, 1)がどこに射影されるかを考えれば算出できる


  • 直交座標系において、直行した基底ベクトルは系の姿勢を表す。つまり、回転行列によってこれらの基底ベクトルを回転させた結果はローカルの系の「姿勢」を表す。基底ベクトルをe1、e2と表わそう。任意の系の基底ベクトルは行列の回転成分から読み取ることができ、これは座標系の姿勢を表す


  • 先ほど回転行列を算出する方法を図示した。軸上の2本のベクトル(1, 0)と(0, 1)はワールド座標空間での基底ベクトルであり、(cosθ, sinθ)と(-sinθ, cosθ)は回転した座標系の基底ベクトルであることを少しだけ意識して見直すと違った世界が見えてくる
  • さて、ここまでの考え方を図にすると以下となる。さきほどと異なり、位置と姿勢の状態を行列に埋め込んでしまい、そこから取り出すような感覚
    • 例えば行列Mの姿勢は時計回りに90度回転していることが読み取れる


  • 余談だが基底ベクトルは以下の条件を満たしていなければならないので注意が必要だ
    • それぞれの基底ベクトルの長さは1
    • 互いに直行する


  • 合成行列は「移動と回転を合成した」行列なので、「移動と回転を適用した結果の位置と姿勢の情報」を当然持っていると考えるとよいかもしれない
  • さて、90度回転する行列を考えてみよう。サンプルでは右回りであるとしているため、回転の方向を時計回りとしている


  • 同様に時計周りに180度回転する行列は


  • 時計周りに270度回転する行列は



  • では先ほどの例を考えてみよう(a = {1, 4})。a[0]は1であるので、T0


  • また同様にa[0]は1であるので、R0は時計回りに90度回転する行列である


  • これらの行列からM0を求めてみよう


  • 同様にM1


  • では合成行列Mを求めてみよう


  • 合成行列Mを移動行列と回転行列の積に分解する


  • 右辺の初めの行列が移動行列、後の行列が回転行列である


  • 行列Tと行列Rを眺めると、行列Tは位置(4, 1)を表し、行列Rは時計回りに90度傾いた姿勢となっていることが分かる
  • プログラムを何回か適用した結果は、Mの積を取ることで求めることができる


  • 以下に図を再掲する。見比べると楽しい


  • 演算にベクトルが全く登場しないことに気付いただろうか
    • ベクトルを登場させる必要がないというか、むしろベクトルを登場させると姿勢の情報が欠落してしまうのだ
  • 実際にSRMで自分がこれを繰り出せるのはまだまだ先のことだろう(ないかもしれない)。だが、この考察はとても楽しかった!