AtCoder Biginner Contest 099 C Strange Bank

なかなかおもしろい問題が出たので、主な考察の流れを書いておこうかなと思います

問題

https://abc099.contest.atcoder.jp/tasks/abc099_c

ある銀行では、お金の引き出しを難しくするために、一回の操作で引き出せる金額が以下のいずれかとなっています。

1 円

6 円、6^2(=36) 円、6^3(=216) 円、…

9 円、9^2(=81) 円、9^3(=729) 円、…

この銀行からちょうど N 円を引き出すには少なくとも何回の操作が必要か求めてください。

ただし、一度引き出したお金を再び預け入れてはならないとします。

1 <= N <= 100000

考察

自分が意識していることの一つに、

「一気に二つのことをしようとするな」

なので、これも

「一気に6nと9nを扱おうとするな」

と考えて解きました(状態をうまく縮約できるときもありますが、難しい場合が多い(それはAGC))

それぞれで考えます

6nだけ使える場合

このときは N を6進数で表示したときの桁の総和が答えです

9nだけ使える場合

このときは N を9進数で表示したときの桁の総和が答えです

これを踏まえると....

6進数で扱う分と9進数で扱う分で分けてそれぞれの総和を足せばイケそう...?

となるのでそれをします

O(NlogN)で解くことができましたね

https://abc099.contest.atcoder.jp/submissions/2649449

クソコード

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
#define rep(i,s,e) for(int (i) = (s);(i) <= (e);(i)++)

int N;

int main(){
  cin >> N;
  int result = N;
  for(int n = 0;n <= N;n++){
    int nn = n;
    int m = N - n;

    int ans = 0;
    int nine = 1;
    while(nine * 9 <= nn) nine *= 9;
    while(nine != 1){
      ans += nn / nine;
      nn %= nine;
      nine /= 9;
    }
    ans += nn;
    nn = m;
    nine = 1;
    while(nine * 6 <= nn) nine *= 6;
    while(nine != 1){
      ans += nn / nine;
      nn %= nine;
      nine /= 6;
    }
    ans += nn;
    result = min(ans , result);
  }

  cout << result << endl;
}

もちろんDPでも解けますね...

vimでC++書くんだったら vim-lsp + cquery 使おうぜ

つい最近clangdについて記事を書きました

kutimoti.hatenablog.com

なんか矛盾してませんか

clangd,とっても優秀なのですが

・別ファイルにある定義先に飛べない
・メモリ消費量がエグい(6GB...)

などの欠点がありました

実はclangdを導入する前にcqueryを使う予定でしたが、そのときはうまく行かなかったので、ここに記したいと思います

cqueryの導入

brewやaurを使ってインストールすると、バージョンが低いためにvimでエラーを吐きまくる事態になっていたので、直接 build したいと思います

github.com

基本ここの、"Build the language server" に従って行きますが

clangとllvmがインストールされているなら

github.com

ここにあるように

1 $ git clone https://github.com/cquery-project/cquery --single-branch --depth=1
2 $ cd cquery
3 $ git submodule update --init && ./waf configure --variant=system --llvm-config=/usr/bin/llvm-config build

としたほうが良さそうです

あとは,cquery/build/system/binにパスを通します

vimに導入

ひゃひゃ

[[plugins]]
repo='prabirshrestha/async.vim'

[[plugins]]
repo='pdavydov108/vim-lsp-cquery'
hook_add='''
autocmd FileType c,cc,cpp,cxx,h,hpp nnoremap <leader>fv :LspCqueryDerived<CR>
autocmd FileType c,cc,cpp,cxx,h,hpp nnoremap <leader>fc :LspCqueryCallers<CR>
autocmd FileType c,cc,cpp,cxx,h,hpp nnoremap <leader>fb :LspCqueryBase<CR>
autocmd FileType c,cc,cpp,cxx,h,hpp nnoremap <leader>fi :LspCqueryVars<CR>
'''

[[plugins]]
repo='prabirshrestha/vim-lsp'
hook_add='''
if executable('cquery')
   au User lsp_setup call lsp#register_server({
      \ 'name': 'cquery',
      \ 'cmd': {server_info->['cquery']},
      \ 'root_uri': {server_info->lsp#utils#path_to_uri(lsp#utils#find_nearest_parent_file_directory(lsp#utils#get_buffer_path(), 'compile_commands.json'))},
      \ 'initialization_options': { 'cacheDirectory': '/tmp/cquery/cache' },
      \ 'whitelist': ['c', 'cpp', 'objc', 'objcpp', 'cc'],
      \ })
endif
let g:lsp_signs_enabled = 1         " enable signs
let g:lsp_diagnostics_echo_cursor = 1 " enable echo under cursor when in normal mode

let g:lsp_signs_error = {'text': '✗'}
let g:lsp_signs_warning = {'text': '‼'}

let g:asyncomplete_completion_delay=10
'''

[[plugins]]
repo='prabirshrestha/asyncomplete.vim'

[[plugins]]
repo='prabirshrestha/asyncomplete-lsp.vim'

compile_command.json

今話題の†json

github.com

これを見てやってもらうとプロジェクト内でもいい感じにcqueryが動いてくれます

単一ファイルで補完が出したい

競プロerの僕は一個のファイルだけを編集するので

それに合わせてcompile_command.jsonを構成しないといけません

いちいち書くのは面倒なので

function MakeCquery()
  let temp = expand('%:p')
  echo system('echo ''[{"directory": "/home/kutimoti/contest","command": "/usr/bin/c++  ' . temp . ' -std=c++11","file": "' . temp . '"}]'' > compile_commands.json')
endfunction

とinit.vimにしてやって

call MakeCquery()

としてセッティングしています

楽しいLSPコーディング生活を!

vim8,neovimで補完使うならdeopleteよりvim-lsp使おうぜ

つい最近,vscodeC++導入の記事を書きました

kutimoti.hatenablog.com

vscodeMicrosoftが作ったC++拡張機能にはLanguage Server Protocol(LSP)という技術が使われています

qiita.com

LSPは他の言語にももちろん対応しています(C#のOmniSharpとか)

これが補完などの処理を行ってくれます

neovimのC++補完

今まで僕は,deoplete-clangやdeoplete-xclangを使ってきましたが,

「(多分)毎回"clang"の呼び出しを行っているのが原因で補完などの動作がvscodeに比べて遅い」

印象を受けていました

vimでもLSPを使うものが無いかと調べると出てきました

github.com

(作ってる人がMicrosoftの方)

ここを見れば簡単に導入できると思います

install

まずclangdをインストールします

sudo pacman -S llvm clang-tools-extra

deinを使ってインストールしています(それぞれの環境に合わせてインストールしてください><)

[[plugins]]
repo='prabirshrestha/async.vim'

[[plugins]]
repo='prabirshrestha/vim-lsp'
hook_add='''
if executable('clangd')
    au User lsp_setup call lsp#register_server({
        \ 'name': 'clangd',
        \ 'cmd': {server_info->['clangd']},
        \ 'whitelist': ['c', 'cpp', 'objc', 'objcpp'],
        \ })
endif

let g:lsp_signs_enabled = 1         " enable signs
let g:lsp_diagnostics_echo_cursor = 1 " enable echo under cursor when in normal mode

let g:lsp_signs_error = {'text': '✗'}
let g:lsp_signs_warning = {'text': '‼'}
<i></i>
'''

[[plugins]]
repo='prabirshrestha/asyncomplete.vim'

[[plugins]]
repo='prabirshrestha/asyncomplete-lsp.vim'

[[plugins]]
repo='prabirshrestha/asyncomplete-neosnippet.vim'
hook_add='''
call asyncomplete#register_source(asyncomplete#sources#neosnippet#get_source_options({
    \ 'name': 'neosnippet',
    \ 'whitelist': ['*'],
    \ 'completor': function('asyncomplete#sources#neosnippet#completor'),
    \ }))
imap <C-k>     <Plug>(neosnippet_expand_or_jump)
smap <C-k>     <Plug>(neosnippet_expand_or_jump)
xmap <C-k>     <Plug>(neosnippet_expand_target)
'''

補完

f:id:Kutimoti:20180520105926p:plain

Rename

f:id:Kutimoti:20180520110006p:plain

f:id:Kutimoti:20180520110020p:plain

f:id:Kutimoti:20180520110030p:plain

エラー表示

bits/stdc++11.hをインクルードしても全く重くならなかったのですごい(すごい)

他にもコマンドはあるのでぜひ使ってみてください

格子状に座標が分けられてDFS,BFSするとき

絶対既存(調べ方がわからない)

なんかよくわからないタイトルになってしまったけど()

ちょっとしたテクも書いていこうかなみたいな備忘録

問題

codeforces.com

問題概要

W * Hの大きさの板チョコがあり、格子に沿って切ろうとしている

(xi1,yi1)から(xi2,yi2)に切り、必ず一つのピースが2つのピースに分かれるように切る

N回切ったときに存在するN + 1個のピースの大きさをソートして出力する

見られた解法

進む方向に切った部分があるのか、無いのかを判定して進む方法

O(WHn)なので十分間に合うけど...

判定めんどい(これが実装力のなさ)ので

2 * 2 の

oo
oo

のような板チョコを

+++++
+o+o+
+++++
+o+o+
+++++

のようにしてやると、

+は辺

oはチョコみたいにできるので、カットがかんたんにできますね(+の対応する部分を通れないようにする)

あとは座標(x,y)の2つの値がどちらも奇数のところがチョコの部分なので、それをカウントすればOKですね

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
#define rep(i,s,e) for(int (i) = (s);(i) <= (e);(i)++)

int W,H;
int N;
vector<vector<int>> fie;
using P = pair<int,int>;

int dx[] = {0,0,1,-1};
int dy[] = {1,-1,0,0};

int bfs(int sx,int sy){
    int ans = 0;
    queue<P> que;
    que.emplace(sx,sy);
    fie[sx][sy] = 0;
    while(!que.empty()){
        int x = que.front().first;
        int y = que.front().second;
        que.pop();

        if((x % 2 == 1) && (y % 2 == 1)) {
            ans++;
        }
        for(int i = 0;i < 4;i++){
            int nx = x + dx[i];
            int ny = y + dy[i];
            if(!(1 <= nx && nx <= W * 2 - 1)) continue;
            if(!(1 <= ny && ny <= H * 2 - 1)) continue;
            if(!fie[nx][ny]) continue;
            fie[nx][ny] = 0;
            que.emplace(nx,ny);
        }
    }
    return ans;
}

int main(){
    cin >> W >> H >> N;
    fie.resize(W * 2 + 1,vector<int>(H * 2 + 1,1));
    rep(i,0,N - 1){
        int x1,y1,x2,y2;
        cin >> x1 >> y1 >> x2 >> y2;
        if(x1 == x2){
            rep(y,y1 * 2,y2 * 2){
                fie[x1 * 2][y] = 0;
            }
        }
        else{
            rep(x,x1 * 2 ,x2 * 2){
                fie[x][y1 * 2] = 0;
            }
        }
    }
    vector<int> ans;
    rep(i,1,2 * W - 1){
        rep(j,1,2 * H - 1) {
            if (fie[i][j]) {
                ans.push_back(bfs(i, j));
            }
        }
    }

    sort(ans.begin(),ans.end());

    for(int i = 0;i < ans.size();i++){
        cout << ans[i] << " \n"[i + 1 == ans.size()];
    }
}

(実はもっとうまくかける(外枠を通れないようにしておけば、範囲の指定なんて要らない))

ICPCのこの問題も同じようにして解いてますねー

Amazing Mazes | Aizu Online Judge

僕のコード

AIZU ONLINE JUDGE: Code Review

ARC038-C 茶碗と豆 -Segment Tree上の二分探索-

才能がないね。悔しい

https://beta.atcoder.jp/contests/arc038/tasks/arc038_c

100点解法

grundy数を使い、遷移は i - C[i] ~ i - 1だけ注目すればOK

104点解法

高速化すべきは遷移にO(N)かかる部分である

ここでseg[i]を「grundy数がiである茶碗の番号の最大値」とする

そして、†Segment Tree上の二分探索†を行う

わけがわからないので、概略を言うと

seg[k * 2 + 1]とseg[k * 2 + 2]の値を見て、深く進んでいく

というイメージです(は?)

seg[i]を求めたいとします

grundy数は[0,N)の中にあるので順番に絞っていきます

もしこのようなgrundy数が[0,N / 2)の範囲にあるとすれば,どのような条件を満たすでしょうか

それは

[0,N / 2)の範囲でのseg最小値がi-C[i]より小さい

です

grundy数の求め方を考えると、範囲に無い数で最も小さい数がgrundy数になるので、この条件になります

これを繰り返し,l + 1 == r を満たしたら終了です

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
#define rep(i,s,e) for(int (i) = (s);(i) <= (e);(i)++)

vector<int> grundy;


struct Segment{
    vector<int> node;
    int n = 1;

    Segment(int sz){
        while(n < sz) n <<= 1;

        node.resize(n * 2 - 1,-1);
    }

    void update(int i,int x){
        i += n - 1;
        node[i] = x;

        while(i > 0){
            i = (i - 1) / 2;
            node[i] = min(node[i * 2 + 1],node[i * 2 + 2]);
        }
    }

    int get(int limit,int k = 0,int l = 0,int r = -1){
        if(r == -1) r = n;
        if(l + 1 == r) return l;

        if(node[2 * k + 1] < limit){
            return get(limit,2 * k + 1,l,(l + r) / 2);
        }
        return get(limit,2 * k + 2,(l + r) / 2,r);
    }
};


int N;
vector<int> C;
vector<int> A;

int main(){
    cin >> N;
    C.resize(N);
    A.resize(N);

    rep(i,1,N - 1){
        cin >> C[i] >> A[i];
    }

    int gs = 0;
    Segment seg(N + 10);
    seg.update(0,0);
    rep(i,1,N - 1){

        int ok = seg.get(i - C[i]);

        if(A[i] & 1) gs ^= ok;


        seg.update(ok,i);
    }

    if(gs == 0){
        cout << "Second" << endl;
    }
    else{
        cout << "First" << endl;
    }

}

ほかの例題があったら練習したいねこれ

AtCoder青色になるまでにやったこと

ありがとうございます ありがとうございます ありがとうございます

f:id:Kutimoti:20180416000716p:plain

水色から青色になるまでにやったことメインで書きたいと思います

緑後半、水色前半の方に届いたらいいなーという気持ちです

終始適当なテンションなのでご了承下さい

絶望のJOI本選

100点台....ここで自分の無力さ、努力のしなさ、ゴミであることを知りました(無知の知)

このころからTwitterが病んできます

春休みの精進

一週間で約100ACを生やす精進生活を送っていました()

f:id:Kutimoti:20180416000753p:plain

AtCoderで精進するのも良いですが、CodeForcesICPCにも手を出しました

ライブラリ作成

すごくいい経験になります(C++が書けるようになる)

GitHubのいい練習です(これは嘘でpushしかしない)

どんなお気持ちで精進していたか

精進し始めは、コーディング力も何もかも足りない状態だった(緑、水色前半とか抱えてるかもしれない問題)ので、克服するべく、ABC-C,D,ARC-Bを埋めました

実装力も付いてきたところで、典型力のNASAを感じるようになりました

ここでCodeforcesに手を出します

すごくお勉強になります(問題を理解すると簡潔な問題になる=典型的な考察,テクの必要性)

ICPCにも取り掛かりました(実装問題は捨てて、考察問題を解きました)

何回も「考察を意識して」繰り返してくるうちに、考察力も付いてきて、AGCが解けるようになってきました(個人差、経験で頭を磨いた感じ)

そのうちに青になることが出来ました

正直、僕は周りに比べて「天才」じゃなかった

(ポエム要素)

僕のFFには「青になるまでに特にしたことが無いから記事書けねぇ」とか、「青は通過点」みたいな人がいて、そういう人が青になるのかと感じてはいました

正直、そういう人たちを見て悔しかったし、何回も病みました.

週末のARCの結果が悪いと、一週間病みっきりです()

「あーなんで解けるんだろう...」

そのたびに憧れて努力をし続けようと思い、今に至る感じです(要は嫉妬で強くなった)

まあ、「努力の量で実力が変わるなんて思うな」ってエアリプが来た時は正直萎えましたが((((

人から否定されても、それが未確定要素ならそれを自分で確かめたほうがいいです(親にPCの前で座っているだけとか言われても気にするんじゃないぞ)

これから

codeforces詰める

区間DPを勉強してみた

精進してるとやっぱり出てきた区間DP...

避けてきたけど、今回は少しコツをつかもうと思って勉強してみました

What's 区間DP?

区間についての動的計画法です。はい。

....

は?

訳がわからんから取り敢えず問題解いてみよう

だるま落としぃ

http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1611&lang=jp

どのようにブロックの対を取り出せばいいのか....っていうところなんですが...

まずはdpの保持する値を決めてしまいましょう

dp[l][r] := 区間[l , r)で取り除くことのできるブロックの数

どのように求めるか

2つパターンがあって、今見ている区間を[l,r)とすると

1.lのブロックとr - 1のブロックが対になれる
2.[l,mid) , [mid , r)に区間を分ける

1.が満たすのはどういう状況か...

それは

・w[l]とw[r - 1]の差が1
・[l + 1,r - 1)が完全に取り除ける
    -> dp[l + 1][r - 1] が r - l - 2 

という条件にあたりますね

[l] [......] [r]

という配置のとき

[......]

が弾き出すことができて,かつ

[l][r]

をはじき出せるかということになります

2.を考えると

//左側で取り除けた量 + 右側で取り除けた量
dp[l][r] = dp[l][mid] + dp[mid][r]

というふうに出来ます

これをDPすればできちゃいそう!

どうやって書こう

再帰で書く方法と,ボトムアップ(for文だけのやつ)で書く方法,

どちらもありますが、再帰で書いたほうがわかりやすいかも(個人差)

int rec(int l,int r){
    //既に計算済み?
    if(dp[l][r] != -1) return dp[l][r];

    //これ以上取り除けない?
    if(abs(l - r) <= 1) return 0;

    int res = 0;
    //パターン1.
    if(abs(w[l] - w[r - 1]) <= 1 && rec(l + 1,r - 1) == r - l - 2)
    {
        //[l , r)がはじき出せるので
        res = r - l;
    }

    //パターン2.区間を分ける
    for(int mid = l + 1;mid <= r - 1;mid++)
    {
        res = max(res , rec(l,mid) + rec(mid,r));
    }
    return dp[l][r] = res;
};

こんな感じ

あとはrec(0,n)で求められるね

ひゃい

ボトムアップはこんな感じです

for(int W = 2;W <= n;W++)
{
    //left
    for(int l = 0;l < n;l++)
    {
        int r = l + W;
        if(r > n) continue;

        if(dp[l + 1][r - 1] == W - 2 && abs(w[l] - w[r - 1]) <= 1)
            dp[l][r] = W;

        for(int mid = l;mid <= r;mid++)
        {
            dp[l][r] = max(dp[l][r] , dp[l][mid] + dp[mid][r]);
        }

    }

}

幅を広げていくイメージです

コツ??

結局、遷移が大事だったね(ア)

どのように小さな区間に遷移していくかが問題っぽい...

今回は基本的なことしか書いてないからまだわからないけど

また違う問題で書こう