アンパンマンの呪い

adventar.org

序章

皆さん、憶えているでしょうか?

すぐ濡れて力が出なくなるので極限まで顔の表面積を小さくされたアンパンマン

でぶ詰め合わせ - でぶ脱却日記 (hatenablog.com)

 

国民的ヒーローであるアンパンマンが、shojin_debu という名の男により顔を極限まで小さくされました。

顔を濡らされるならまだしも、ここまで小さくされてはもはや誰なのかよくわかりません。これはアンパンマンにとって今までで一番の屈辱でしょう。

 

さて、この小さな顔を見て何か連想されるものはないでしょうか?

そう、FPSゲームで見られるクロスヘアですね!

簡単に言うと、射撃をするときの照準のことです。

fpsaim.blog.shinobi.jp

 

クロスヘアは点型や十字型のものが一般的ですが、その他にも様々な形のものが存在します。

特にvalorantというFPSゲームではクロスヘアをかなり自由にカスタマイズすることができます。

ここでふと、「アンパンマンの形のクロスヘアで敵をたくさん倒せば、アンパンマンの恨みを晴らせるのでは?」という考えに至りました。

 

アンパンマンクロスヘアを作る

Valorant Crosshair Database というサイトでは多数のユーザーがオリジナルのクロスヘアを作成し、公開されています。

もちろん、アンパンマンの形のクロスヘアを作っていた人はいなかったので作りました。

 

 

  • クロスヘアのイメージ(再現度は、まあ、)

 

 

  • クロスヘアのコピーコード
0;P;c;7;t;2;o;1;d;1;z;3;f;0;0t;10;0l;10;0v;2;0g;1;0o;2;0a;1;0f;0;1t;7;1l;4;1v;0;1g;1;1o;1;1a;0.312;1s;2.479

 

また、このクロスヘアは普通ではありません。キャラクターの停止時には顔を隠し、移動には顔を見せるようにしています(!?)。射撃時にも顔を見ることができます。

 

 

  • キャラクター移動時(射撃時)

 

アンパンマンクロスヘアは強いのか?

アンパンマンクロスヘアを作ったところで、このクロスヘアがどのくらい強いのか検証したいと思います。アンパンマンの呪いを背負っているので、さぞかし強いことでしょう。

とはいえアンパンマンFPSをするタイプではないので、通常時のプレイと比較するのはナンセンスです。検証ではアンパンマンクロスヘア使用時は利き手の右手で、通常時では反対の左手でプレイすることとしました。

 

射撃場での検証

検証では射撃訓練場のチャレンジを使いました。



以下の条件で通常時・アンパンマンクロスヘア使用時それぞれでチャレンジ成功にかかった秒数を記録しました。

  1. キャラクター:ジェット(好きなので)
  2. チャレンジ:ボットを50体排除する時間を競う
  3. ボットはアーマーなし
  4. 弾薬無限
  5. キャラクターの移動は禁止

記録は各条件で3回測定し、平均を比較することでアンパンマンクロスヘアの強さを検証することにしました。

 

  • 通常プレイ時

まあ普通に見やすいですね


クロスヘアが邪魔ですね

 

結果

プレイ条件\測定回 1 2 3 平均
通常プレイ時(非利き手) 59 58 55 57.333...
アンパンマンクロスヘア使用時(利き手) 74 74 69 72.333...

 

わざわざ平均を取るまでもありませんでしたね。アンパンマンクロスヘアの圧勝です。

個人的には非利き手でもそれなりにエイムできたことが驚きです(?)

 

おわりに

これでアンパンマンの呪いがとけると良いですね。

アンパンマンクロスヘアを使ってみたい人はクロスヘアのコピーコードから使えるので、ぜひ試してみてください。

なんでFPSをネタにしたのか、ですか?

それは shojin_debu さんが以前放った「FPSのエイム練習は頭がおかしい」という発言への対抗心かもしれません。まあ彼は普段から暴言が多いので、あまり気にしていませんけれども。

ちなみに私は valorant はほとんどプレイしたことがなく、今回の記事をきっかけに久しぶりにプレイしました。モチベが沸いたらもっとプレイしたいですね。

では皆さん、よいデブライフを。

FPSのエイムについて

apex(初めてプレイしたFPSゲーム)を初めてから1年半ほど経ちます。

実力的には去年のS14にソロダイヤ(上位5%くらい?)になって、最近はよりエイムが良くなりました(ランクをほとんどやっていないのでなんとも言えないですね)。

FPSのエイムについて、自分なりに調べたり考えたりしてそれなりに上手くなりつつあります。そんな中で考えたことをまとめたいというのと、話のきっかけにでもなればというので記事を書くことにしました。どうしても感覚的な部分も入ってきますが、ご了承ください。

内容は主にapexから学んだエイムの話になります。キーボードマウスを使う場合を前提にしてきます。バロラントは最近始めたばかりなのである程度考えがまとまったら記事にしていきたいです。

 

エイムという行為

ゲーム内での操作

エイムとは敵に照準を合わせる行為を指します。ただ、敵に弾を当てるということを考えたときは照準を合わせるというよりは、敵に弾を当てる一連の動作と捉えた方が良いです。

例えばスナイパーなどの遠距離武器を使う際は、敵に弾が届くまでの弾の落下や敵の移動を考える必要があるため、必然的に射撃するタイミングは照準から偏差分を含めたものになります。

またマウス操作の観点からすると、

  • 敵に照準を合わせる
  • 射撃ボタンを押す

の二つの操作は全く別物だということも認識しておきましょう。

 

プレイヤーの動作

それでは、エイムをするにあたって行われるプレイヤーの動作を見ていきます。

基本的に以下の流れになります。

  1. 👀で敵を認識する
  2. クロスヘアと敵を合わせる

当たり前ですが、敵を認識するのは👀です。その後にクロスヘアと敵を合わせる部分は目とマウス(とキーボード)で行います。

つまり、エイムの上達には👀の使い方とマウスの使い方のどちらの上達も欠かせないわけです。またそれぞれの使い方が上手くなるだけでなく、それらを融合させる必要があります。

スポーツをやっていた人はエイムがいいという話をたまに聞きますが、それは(特に球技では)目と身体を一緒に使うことが多いからだと思っています。

 

身体の使い方

目の使い方

目の使い方には大きく

  • 周辺視
  • 中央視

の2つがあります。

中央視は注目して見ることを、周辺視は注目していないが視界に入っているのが見えている状態だと思ってもらえればいいです。

たまに中央視は物にピントを合わせてみることだという説明を見ますが、中央視で見ていてもピントを合わせないことはできるので正確ではないです。

周辺視の方が反応速度は速いですが、正確に見るという面ではピントを合わせられる中央視の方が優れています。

エイムするときは

  1. 周辺視で敵を認識する
  2. クロスヘアを敵の近くに持っていく
  3. (2と並行して) 周辺視から中央視に切り替えながら敵にピントを合わせる

という使い分けをします。

周辺視では敵の存在が分かるだけでなく、その位置を把握できるようにしましょう。

周辺視で敵を見るのが上手くなるとフリック1回で敵とクロスヘアを合わせられるようになってきます。

 

 

※敵にピントを合わせるとは、敵がはっきり見える状態にするということです。敵にピントが合うとスキンの柄が細かく見えたり、敵と目が合ったり、敵と背景との境界が明確になります。

 

※この時に👀とマウスを融合させて使うことができていないと敵にピントが合っているがクロスヘアが敵からずれているという現象が起きます。

敵にピントが合っていないがクロスヘアが敵に合っているということはほぼほぼ起きない(眼球の動きの方がマウス操作よりも正確かつ速いため)と思っています(もしそういう人がいたら教えてください)。

敵の動きがすばやいときに限って敵にピントが合わない、かつクロスヘアが敵からずれている場合、👀とマウスの融合ができているがそれぞれの使い方のレベルがまだ低いと考えられます。

 

マウスの使い方

マウスの持ち方と姿勢

マウスを操作するときに手に加える力は、マウスにポンと手を置いたときの力加減とほとんど変わりません。それ以上はむしろスムーズなマウスの動きを阻害します。

また、マウスを操作する側の腕は肘が肩のラインと同じくらいをスタートポジションになるようにしましょう。可動域の話で、姿勢が悪いとマウスをスムーズに動かせる範囲が狭くなります。例えば左から右に大きく動く敵をエイムし続けることが困難になります。

マウスの動かし方について

マウスの動かし方に意識を向けない方がいいです。

例として以下の2パターンを挙げます。

 

  • マウスの動かし方を覚える場合

敵の動きを勝手に予想し、それに覚えたマウスの動かし方を当てはめるという考え方です。

どういう問題が生じるかというと、

敵が空中で曲がった

→エアストレイフ(空中で1回曲がる)だと思ってその軌道をマウスでなぞる

→実際はツインストレイフ(空中で2回曲がる)動きをされる

というように予想外の動きに対応できないからです。

実際はここまで極端なことはなくても、「こういうマウスの動かし方を良くする」という心当たりがある場合は注意が必要です。

 

  • マウスを動かそうとする場合

マウスを動かそうとすると肩や腕、手首など意識した部位に力を入れることになります。このときに入る力は過剰なことが多いため、続けていると身体のどこかに違和感が生じたり痛みが出たりします。

 

ではどうするかというと、「画面の中央で敵にピントを合わせて見続ける」という意識を持つのが良いと考えています。

一人称視点ではキャラクターの目を動かしているのはプレイヤーの身体(指、手、腕、肩など)です。エイムが上手くなるということは、ゲームの世界にいるキャラクターの目を自由自在に動かせるようになるということです。生まれたときから今まで目の筋肉をどう使って物を見るのか意識している人はいないと思います。つまりゲームの世界で見ることに集中すれば、プレイヤーの身体はそのために筋肉の使い方を無意識に学習するということです。

やるべきことはマインドと身体のコンディションを整えることです。

 

その他

他にもエイムについて書けることは多いですが、今回書きたかった部分(エイムをするときの身体の使い方)については書けたので、必要があれば以下を追加する予定です。

  • ラッキングエイムとフリックエイム
  • エイムとキャラコン
  • 武器ごとのエイムの仕方
  • 視点移動の癖の修正
  • クロスヘアなしでのプレイ
  • リコイル

Atcoder 黄色になりました

f:id:unosss:20210922191305p:plain

f:id:unosss:20210922191354p:plain

f:id:unosss:20210922191412p:plain

f:id:unosss:20210922191423p:plain

以下は備忘録です。

○精進で意識したこと

・負荷をかける(時間をはかる、バチャで人と競う、自分の色以上の問題を解く)

・時間がかかってもいいから解説ACをせずに解く(AGCの黄diffあたり)

・工夫できるところを探す(バグを生みにくい実装、簡潔な実装、問題を解く順番、考察で詰まった時にどうするか)

○コンテストで意識したこと

・No subをしない(出られるコンテストには出た方が将来的には強くなると信じています)

・考察が進まなかったりバグが取れなかったりするときはちょっと休憩を挟む

・FAの時間や解いてる人の傾向から解法を予測する(結構効果がありました)

・最後の1秒まであきらめない 絶対にTwitterは見ませんでした

○黄色になった感想

・少なくとも自分は全然強くない

・成長の仕方は人それぞれ でも成長しようと努力するのはとても大事

・競プロ楽しい

・まだまだ強くなれそう

・橙コーダーになりたい

○お世話になった競プロer

・nok0さん・・・バチャ楽しいです。ありがとうございます。

・SSRSさん・・・ツイートを見ているだけでレートが上がっている実感があります

○好きな競プロer

数えきれませんが、特に好きな方

maspyさん、beetさん、のしさん、noimiさん、tatyamさん、うしさん、タプリスさん、らてさん、がぜるさん、PCTさん、ぷらにゃ、someone、kaikey、れたす、haira、ヒトデマン、そすうぽよ、にう。。。

 

次は橙を目指します!

EDPC T-Permutation Skew tableauxを利用した解法

atcoder.jp

問題概要:(1, 2, ..., N)を並び替えた順列(p _ 1, p _ 2, ..., p _ N)であって、次の条件を満たすものは何通りか?10^ 9+7で割った余りを求めよ。
・文字列Sが与えられ、Si文字目が\ltのときはp _ i \lt p _ i+1であり、Si文字目が\gtのときはp _ i \gt p _ i+1である。

この問題はskew tableauxの標準盤の個数の数え上げに言い換えることができます。 hotmanさんの記事を読んでください。

qiita.com

本記事では↑のアイデアのもと実装するパートのみ紹介します(人によってはやるだけ、かもしれません)。

与えられる文字列Sに対してskew tableauxが一意に定まります。 例えば文字列S\lt\gt\lt\gt\ltのとき、skew tableauxは以下のような図形になります。

f:id:unosss:20210404141402p:plain

求めたいのはこのskew tableauxへの数字の書き込み方の個数(標準盤の個数)です。以下のような手順で求めることができます。

skew tableauxをもとにyoung tableauxを構成します。

f:id:unosss:20210404141626p:plain

このyoung tableauxについて、各マスのフック長を書き込みます。

f:id:unosss:20210404143359p:plain

このyoung tableauxにおいて、最左下のマスから最右上のマスへの最短路を考えます。

f:id:unosss:20210404143834p:plain

例えば上図のような経路において、経路の長さをNとしたとき

val = \frac{N!}{(各マスのフック長hの積)}= \frac{6!}{1 \times 2 \times 3^ 2 \times 4 \times 5}

がこの経路が寄与する標準盤の個数です。

よって、young tableauxにおける最短路の集合をUとしたとき、求める標準盤の個数は

ans = \sum_{ u \in U} val(u)

となります。

最短路の個数が膨大になるため、最短路ごとにvalを求めていると間に合いません。

そこで、最左下のマスからスタートする前にnow = N!を持っておき、

マス(i, j)に移動したときにnow /= h(i, j)(マス(i, j)のフック長)と更新します。

 dp(i, j)= \frac{dp(i-1, j) + dp(i, j-1)}{h(i, j)}という要領です。)

こういう工夫の仕方は以前Atcoderで出題されていましたね。

atcoder.jp

最後に実装例を挙げておきます。

#include <bits/stdc++.h>
using namespace std;

using ll = long long;

#define REP(i,n) for(ll i = 0;i < (n);i++)

const ll mod = 1e9+7;

struct mint {
  ll x; // typedef long long ll;
  mint(ll x=0):x((x%mod+mod)%mod){}
  mint operator-() const { return mint(-x);}
  mint& operator+=(const mint a) {
    if ((x += a.x) >= mod) x -= mod;
    return *this;
  }
  mint& operator-=(const mint a) {
    if ((x += mod-a.x) >= mod) x -= mod;
    return *this;
  }
  mint& operator*=(const mint a) { (x *= a.x) %= mod; return *this;}
  mint operator+(const mint a) const { return mint(*this) += a;}
  mint operator-(const mint a) const { return mint(*this) -= a;}
  mint operator*(const mint a) const { return mint(*this) *= a;}
  mint val(){
    return x;
  }
  mint pow(ll t) const {
    if (!t) return 1;
    mint a = pow(t>>1);
    a *= a;
    if (t&1) a *= *this;
    return a;
  }
 
  // for prime mod
  mint inv() const { return pow(mod-2);}
  mint& operator/=(const mint a) { return *this *= a.inv();}
  mint operator/(const mint a) const { return mint(*this) /= a;}
};
istream& operator>>(istream& is, mint& a) { return is >> a.x;}
ostream& operator<<(ostream& os, const mint& a) { return os << a.x;}
// combination mod prime
// https://www.youtube.com/watch?v=8uowVvQ_-Mo&feature=youtu.be&t=1619
struct combination {
  vector<mint> fact, ifact;
  combination(ll n):fact(n+1),ifact(n+1) {
    assert(n < mod);
    fact[0] = 1;
    for (ll i = 1; i <= n; ++i) fact[i] = fact[i-1]*i;
    ifact[n] = fact[n].inv();
    for (ll i = n; i >= 1; --i) ifact[i-1] = ifact[i]*i;
  }
  mint operator()(ll n, ll k) {
    if (k < 0 || k > n) return 0;
    return fact[n]*ifact[k]*ifact[n-k];
  }
  mint p(ll n, ll k) {
    return fact[n]*ifact[n-k];
  }
} c(3005);

void main_() {
    ll N;
    cin >> N;
    string s;
    cin >> s;
    ll a = 0,b = 0;
    REP(i,N-1){
        if(s[i] == '<')a++;
        else b++;
    }
    
    vector<vector<mint>> dp(b+1,vector<mint>(a+1));
    
    //sumには各マスの下にあるマスの数、lenには各行の列の個数、sumには各マスのフック長
    vector<vector<ll>> num(b+1,vector<ll>(a+1)),sum(b+1,vector<ll>(a+1));
    vector<ll> len(b+1);
    sum[0][0] = 1;
    ll h = 0,w = 1;
    
    REP(i,N-1){
        if(s[i] == '<'){
            sum[h][w] = 1;
            w++;
        }
        else{
            len[h] = w;
            h++;
        }
    }
    
    len[h] = w;
    REP(i,a+1){
        for(ll j = 1;j <= b;j++){
            if(sum[j-1][i]) sum[j][i] = sum[j-1][i] + 1;
        }
    }
    mint val = c.fact[N];
    REP(i,b+1){
        REP(j,a+1){
            num[i][j] = sum[i][j] + len[i] - j - 1;
        }
    }
    dp[0][0] = val;
    dp[0][0] /= num[0][0];
    
    REP(i,b+1){
        REP(j,len[i]){
            if(i == 0 && j == 0)continue;
            if(i){
                mint v = dp[i-1][j];
                v /= num[i][j];
                dp[i][j] += v;
            }
            if(j){
                mint v = dp[i][j-1];
                v /= num[i][j];
                dp[i][j] += v;
            }
        }
    }
    cout << dp[b][a] << endl;
}
 
int main() {
    int t = 1;
    //cin >> t;
    while(t--) main_();
    return 0;
}

Atcoder 青になりました

区切りなので書くことにしました。

f:id:unosss:20210319192303p:plain

f:id:unosss:20210319192346p:plain

f:id:unosss:20210319192416p:plain

〇水⇒青で思ったこと

・入水⇒水上位まではなんか自動的にレートが上がった。

・水上位で停滞(多分青diffがあまり解けないとこうなる)。

・無限人に抜かされた。。。

〇やって良かったこと

・前やった問題を解きなおしてみる

以前解けずに解説ACした問題、意外と忘れてることが多かったので勉強になりました(色んな解法で解いてみたり)。レートが上がってからの方が理解が深まっている気もしています。

・コドフォバチャをする

水にはちょうどいい問題が多いです。

・無理をしないでたくさん寝る

入水した辺りはたくさん問題を解きたくて睡眠時間削ってまで精進したりしてましたが、むしろ逆効果だったような気がしています。無理をしないでたくさん寝るようにしてから青パフォは安定して取れるようになってきました。

〇やらない方が良かったこと

・お酒を飲んでRatedのコンテストに出る

こないだのABC195、友人の結婚式でお酒を飲んだ後に出て冷えました。お酒を飲むと本当に頭が回らなくなってびっくりです。反省しています。

 

最近は精進の度にまだまだ基本ができていないと分からされて悔しいですが、少しずつ前に進んでいると信じています。

絶対黄色になります。

2/5 日記

え、今日1問しかといてないのか 絶句

・N マス目のなんたら

bitDP。普通に上から一つずつやると中央値の判定がうまくいかない。i行目の状態を表すbit列をbiとしたとき、bi-1、bi+1が存在する事がbiの成立条件となる。よって、

dp[j][b]を、j行目まで見たときに、j-1行目とj行目のbit列をbとし、j-1行目について中央値の条件が成り立つものの個数とすればよい。

マス目の外は全て0なので、このdpを自然に更新していけば解ける。


すばすば

2/4 日記

眠すぎる

応用情報、勉強する時間があるのか謎。。。

N - 入れ替えと並び替え (atcoder.jp)

解説ACした。Q回のswapで増加する転倒数は最大でQであり、ソートクエリのときは合計高々Q回のswapを行うことにより計算量が抑えられるというもの。

A[x]>A[x+1] を満たすxをsetで管理すれば、ソートクエリのときに二分探索でa <= x <= bとなるような最小のxを求めることができ、間に合う。

L - スーパーマーケット (atcoder.jp)

実装がやや重かった。

aの値が1か2なので、1列目と2列目をなんらかのデータ構造(データの削除、追加、検索、最大値の取得が高速にできるもの)で保持すればよい。自分はpriority_queue+Tの値を座圧して配列に情報を保持、でやったけど、setでやったほうが実装がやや軽くなるみたい。明日実装しようかな。

O - 宝箱 (atcoder.jp)

最終問題にしてはそこまで難しくなかった。

dp[i]:=i番目まで見たときのY-Xの最大値

というdpはすぐに思いつく。普通に更新するとO(NM)かかる。

ここで、j番目の鍵屋はl j ~ r jまでをcost jで開錠できるという情報を、イベントソートする要領で処理したくなる(イベントの追加と削除ができればdpの更新は簡単)。

イベントを追加するとき、イベントの位置をposとすると、posまでの宝箱をすべて開けたときに得られる合計金額をnowとする。すると、setに(dp[pos-1]-now-cost)を追加すれば、位置iに来た時にsetの最大値をMXとして

now+MXが答えとなることが分かる。

 

ねんねぽよ