naoppyの日記

自分が欲しいなと思った記事を書きます。

Javaでの二分探索、lower_boundとupper_boundの実装

こんにちは、naoppyです。 AtCoderでこの問題 C - Snuke Festival を解いているときに、Javaの二分探索が能力不足だと感じた為、自作することにしました。

Javaの二分探索

Collections (Java Platform SE 8) これを見ればわかるのですが、Javaの二分探索は引数にlist、key、Comparatorをとります。

戻り値はintで、見つかった場合はkeyのインデックス(複数のkeyがリストに含まれている場合、どれが返るかは保障されない)

見つからなければ(-(挿入ポイント) - 1)が返ります。

(-(挿入ポイント) - 1)、これはどういうことかというと、見つからなければ必ず戻り値は0未満、負数になるので、見つかったかどうかがわかります。

ここから挿入ポイントを復元するには逆の操作、(-(戻り値+1))をしてやればいいです。

もう少し考えてみましょう。1足してマイナスにする=1足して2の補数を取る=1足してbit反転してから1を足す=bit反転だけでいいことになります。

よって(-(挿入ポイント) - 1)から挿入ポイントを得るには~value(~はbit反転)でいいことがわかります。

lower_bound、upper_boundとは

C++のstd::lower_bound、std::upper_boundのことです。 lower_bound関数は指定した値以上の要素が最初に現れる位置を返し、upper_bound関数は指定した値より大きいの要素が最初に現れる位置を返します。

実装の方針としては、二分探索の引数の3番目、Comparatorを自分でうまくかくことで、比較的簡単に書こうと思います。

ラムダ式を使うことでさらに簡単に書けます。

実装

まず、Comparatorだけの実装をお見せします。

Comparator<Integer> lower = (x, y) -> x.compareTo(y) >= 0 ? 1 : -1;
Comparator<Integer> upper = (x, y) -> x.compareTo(y) > 0 ? 1 : -1;

これをCollections.binarySearch(list, key, Comparator)の第3引数にわたしてあげれば、それぞれlower_bound、upper_boundになるわけです。

ですが、このComparatorを渡したbinarySearchは必ず負数を返すことになります。よって値が存在したかどうかはわかりません。

戻り値は必ず~を付けてbit反転しましょう。

使用例

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class binarySearch {
    final static int[] array = { 2, 2, 3, 4, 4, 4, 6, 6 };
    final static List<Integer> list = int2List(array);

    public static void main(String[] args) {
        //lower_bound
        int lowerResult = ~Collections.binarySearch(list, 4, (x, y) -> x.compareTo(y) >= 0 ? 1 : -1);
        System.out.println(lowerResult);// => 3
        //upper_bound
        int upperResult = ~Collections.binarySearch(list, 4, (x, y) -> x.compareTo(y) > 0 ? 1 : -1);
        System.out.println(upperResult);// => 6
    }

    //int配列をList<Integer>に変換するヘルパーメソッド
    static List<Integer> int2List(int[] arr) {
        List<Integer> list = new ArrayList<>();
        for (int i : arr) {
            list.add(i);
        }
        Collections.sort(list);
        return list;
    }
}

最後に

  • ~が見にくい。見落としやすくバグを生む危険がある。

  • binarySearchの引数に直接ラムダ式を渡すのは、可読性の面から良くない。 Comparatorを作って、それを渡す方が良い。

  • 値の存在を確認するのは、普通のbinarySearchを使って戻り値が正かどうかを見るしかない。

など、気を付けるべき点がいくつかある。

それなりに綺麗にかけたので満足。

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

naoppyです。4/21にAtCoderで水色になることが出来ました。 一つの節目として、やってきたこととかをまとめます。

レート変移

f:id:naoppy_ac:20180422002045p:plain これを見てわかるのは、まあ僕に特別な才能や強い考察力がないことぐらいでしょう。 31回も参加してます。

水色時点での自分

実力と言っても色々パラメーターがあるので、難しいですが、僕のスペックを並べます。

  • プログラミング歴1年、Java使い
  • 競プロ歴は始めたのが去年(2017)の7月中旬、それなりに本気でやり始めたのが12月ぐらいです
  • 1月ごろに300点問題は完全に安定、水色になった時点で400点が半分解けるぐらいの実力

自己紹介かよ

使えるアルゴリズムとテク

  • 一次元累積和
  • エラトステネスの篩
  • 最小公倍数と最大公約数
  • bit全探索
  • BFS
  • DFS
  • 二分探索
  • UnionFind
  • 重み付きUnionFind
  • ワーシャルフロイド
  • ダイクストラ
  • ベルマンフォード
  • nCr

これだけです、こうして見ると少ないですね。 僕は何をしていたんだ

ちなみにDPはできません!!!DPできなくても水色にはなれます。

ただDPの考え方は知っておくべきかもしれないです。

Twitterがやめられない

何度ツイ禁しようとしても辞められません。 ひたすらに時間が吸われます。

結局ツイ廃状態のまま水色になれましたが、青になるにはそれなりにTwitterから離れた方がいいのは自明です。

精進方法

AtCoderProblemsで過去問を解く

誰もが使っているであろう AtCoder Problems

水色になった時は殆どABCしか埋めていませんでした。

ABCのCは80%、Dは23%ほど埋めていました。

TwitterのDMで優しい人に聞きまくる

普通に迷惑なのですが、優しい方に詰まっている問題の考察過程などを教えていただきました。 感謝しかありません。

まあこんな具体的なこと書いても誰の参考にもならないのでまとめると、優しい人(そこら中にいるはず)に特に考察過程を聞きましょう。

考察力は自分ではなかなか伸ばしにくいスキルです、精進への近道だと思います。考察力は大事です。

チーター本を読む

蟻本、チーター本等を読みましょう。チーター本は特に初心者におすすめです。

ちなみに蟻本は持ってないので、どなたか買ってくれる人を募集中です。

スライドやQiitaの記事を読む

学びたいアルゴリズム、解説を見たら知らないアルゴリズムだったときは、アルゴリズム名でググりましょう。

強い人のスライドや記事がでてくるはずです。ありがたい...。

思いつくのはこれだけですね、人によってはバチャったり教育的良問を教えてもらって解くなどしているみたいですが、僕はしませんでした。

解くときのテク

まず問題文をよく読みましょう。ある程度アルゴリズムを知っていると、問題からどのアルゴリズムを使えばいいかというのがぱっとわかる問題は多いです。

例を出すと、

  • 平面上での迷路→BFS

  • 頂点を結ぶ→グラフ系のアルゴリズムかDFS

  • Yes、Noの明確な境界がある→二分探索

などですかね。

もしあからさまな典型がでればライブラリをコピペして終わりなのですが、それ以外の時、どうすればいいかというと、まず計算量を推測します。

nlogn、nなら通るけど、n2は通らないな...、など、ある程度分かれば大丈夫です。

次に、問題のネックとなっている部分はどれか(そこさえわかれば解ける場所)を自分の中でしっかり把握します。

そして

  • 制約が簡単な場合を考える、そこから発展させる。

  • サンプルを手でシュミレーションしてみて、自分が考えた過程をそのままプログラムに落とす

  • 式を立てて、変形させる

  • サンプルから、規則性やルールを探し出す

などをすれば、きっとACできるはずです(自己暗示)

まとめ

DPできなくても大丈夫。

海外コン出たこと無くても大丈夫。(無論出た方がいい)

ずっと早解き3完でも大丈夫。

ツイ廃でも多分大丈夫。

これを見て、「俺でもできそう」となれば幸いです。

Brainfuckを中間言語に変換するやつ

最近、自作言語を作ってみたというような人々をよく見る

僕もBF系のクソ言語作るか~ということで、その前準備として中間言語に変換するやつを作ってみた

言語仕様?は+、-、>、<、は末尾に連続して続く数を付ける、ドット、カンマ、[、]、はそのまま、という感じです

あと無駄なコードは最適化されます

+++--は+1、>>><<>は>2といった感じ

内容

こんなBFコードが

+++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.++
+++++..+++.>-.------------.<++++++++.--------.+++.------.--------.>+.

こうなる

+9[>1+8>1+11>1+5<3-1]>1.>1+2.+7..+3.>1-1.-12.<1+8.-8.+3.-6.-8.>1+1.

フォルダ階層

C:/~/BFPREPROCESSOR/    
├─bfsource/
│      helloBF.bf
│      
├─bin/
│      FirstOptimize.class
│      
├─middleLang/
        helloBF.mbf

使い方

java bin/FirstOptimize helloBF.bf

のように打つとmiddleLang/にhelloBF.mbfというファイル名で変換される

実装

いつものコードコピペの垂れ流しである...

import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;

public class FirstOptimize {
    static Deque<Character> deq = new ArrayDeque<>(200);
    static BufferedWriter bw;

    public static void main(String[] args) {
        if (args.length < 1) {
            throw new IllegalArgumentException("needs sourcefile name");
        }
        String input = args[0];
        String filename = getFileName(input);
        try {
            bw = new BufferedWriter(new FileWriter("middleLang/" + filename + ".mbf"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("start");
        conversion(input);
        System.out.println("end");
    }

    static String getFileName(String input) {
        String s;
        int dotPlace = input.lastIndexOf(".bf");
        if (dotPlace == -1) {
            throw new IllegalArgumentException("this file is not bf file\nfilename example: helloworld.bf");
        }
        int srasyu = input.lastIndexOf("/");
        srasyu = Math.max(srasyu + 1, 0);
        s = input.substring(srasyu, dotPlace);
        return s;
    }

    static void conversion(String source) {
        try (FileReader r = new FileReader(source)) {
            char c;
            int i;
            while ((i = r.read()) != -1) {
                c = (char) i;
                addStack(c);
            }
            write();
            bw.flush();
            bw.close();
        } catch (Exception e) {
            System.err.println("ファイル読み込み中にエラー発生");
            e.printStackTrace();
        }
    }

    static void addStack(char c) {
        char[] validChars = { '+', '-', '>', '<', '.', ',', '[', ']' };
        boolean flag = true;
        for (char okC : validChars) {
            if (c == okC) {
                flag = false;
                break;
            }
        }
        if (flag)
            return;

        if (!deq.isEmpty()) {
            char lastItem = deq.getLast();
            if (c == '+' && lastItem == '-') {
                deq.pollLast();
                return;
            } else if (c == '-' && lastItem == '+') {
                deq.pollLast();
                return;
            } else if (c == '>' && lastItem == '<') {
                deq.pollLast();
                return;
            } else if (c == '<' && lastItem == '>') {
                deq.pollLast();
                return;
            } else if (c == lastItem && (c == '+' || c == '-' || c == '<' || c == '>')) {
                deq.offerLast(c);
            } else {
                write();
                deq.offerLast(c);
            }

        } else {
            deq.offerLast(c);
        }
    }

    static void write() {
        char c = deq.getLast();
        try {
            if (c == '[' || c == ']' || c == '.' || c == ',') {
                bw.write(String.valueOf(c));
            } else {
                bw.write(String.valueOf(c) + deq.size());
            }
        } catch (IOException e) {
            System.err.println("ファイル書き込み中にエラー");
            e.printStackTrace();
        }
        deq.clear();
    }
}

一応解説

最初はファイル名をえいえいする部分、後は一文字ずつ読んでいく

読んだ文字がBFの有効な8文字なら、スタックに積む

スタックの最後が+で今から積むのが-、など、最適化できるところは最適化する

違う種類が入れられると、ファイルに書き出す

簡単だね、次は中間言語からJavaバイトコードを生成したいけど、ちょっと自分の実力じゃダメそう...

Javaの競プロ?用ライブラリを置いておく

これはgitを使えない人がライブラリをオンラインでどう保管するか考えた結果、記事にコピペするに帰着したクソなので、読むこと見ることを想定していません

基本こっから書く 自動テスト?知らない子ですね...(汗

import java.util.Scanner;

public class Base {
    public static void main(String[] args) {
        final Scanner sc = new Scanner(System.in);
        System.out.println();
    }
}

ベルマンフォード法

import java.util.Arrays;
import java.util.Scanner;

public class Bellman_ford {
    public static void main(String[] args) {
        final Scanner sc = new Scanner(System.in);

        System.out.println();
    }
    //辺を表すクラス
    static class Edge {
        int from;//出発元の頂点
        int to;//到達先の頂点
        int cost;//距離
    }

    static int V;//頂点の数
    static int E;//辺の数
    static Edge[] edges;

    static int[] d;//頂点毎の暫定的な最短距離

    static int INF = Integer.MAX_VALUE / 2;

    /*
    * 始点を指定して各頂点の最短距離を取得する
    * @param sv 始点とする頂点
    */
    static void bellman_ford(int sv) {
        //最短距離の初期化
        d = new int[V];
        Arrays.fill(d, INF);
        d[sv] = 0;

        //更新が終わるまで頂点を繰り返し操作していく
        while(true) {
            boolean update = false;
            for(int i = 0; i < E; i++) {
                int u = edges[i].from;
                int v = edges[i].to;
                int c = edges[i].cost;
                //もし、始点からuまでの最短距離 + uからvまでの距離cが始点からvまでの距離より小さいなら更新する
                if(d[u] + c < d[v]) {
                    d[v] = d[u] + c;
                    update = true;
                }
            }
            if(!update) {
                break;
            }
        }
    }

    static boolean bellman_ford2(int sv) {//負の閉路検出用
        //最短距離の初期化
        d = new int[V];
        Arrays.fill(d, INF);
        d[sv] = 0;

        //更新が終わるまで頂点を繰り返し操作していく
        for(int count = 0; count < V; count++) {
            for(int i = 0; i < E; i++) {
                int u = edges[i].from;
                int v = edges[i].to;
                int c = edges[i].cost;
                //もし、始点からuまでの最短距離 + uからvまでの距離cが始点からvまでの距離より小さいなら更新する
                if(d[u] + c < d[v]) {
                    d[v] = d[u] + c;
                    if(count==v-1) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
}

キューを使ったダイクストラ(正直仕組みわからん)

import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Scanner;

//http://nishiharayuugo.blog.fc2.com/blog-entry-15.html

public class Dijkstra {//頂点数V、辺の数Eに対し、O(E log V)
    public static void main(String[] args) {
        final Scanner sc = new Scanner(System.in);
        System.out.println();
    }

    //候補クラス
    static class Candidate implements Comparable<Candidate> {
        int d;//距離
        int v;//頂点

        public Candidate(int v, int d) {
            this.d = d;
            this.v = v;
        }
        //距離が短いほど優先する
        @Override
        public int compareTo(Candidate o) {
            return this.d - o.d;
        }
    }

    //辺クラス
    static class Edge {
        int to;//どの頂点へ伸びているか
        int cost;//距離

        Edge(int to, int cost) {
            this.to = to;
            this.cost = cost;
        }
    }

    static List<List<Edge>> G;//隣接リストで表現したグラフ

    static int[] dist;//最短距離(仮定も含む)

    static int INF = Integer.MAX_VALUE / 2;

    /*
    * @param sv 始点となる頂点
    */
    static void dijkstra(int sv) {
        //初期化
        Arrays.fill(dist, INF);
        dist[sv] = 0;

        //候補の優先キューを用意
        Queue<Candidate> candidates = new PriorityQueue<>();
        candidates.offer(new Candidate(sv, 0));//最初に始点と距離0の候補を入れておく

        while(!candidates.isEmpty()) {
            //候補を取り出す
            Candidate c = candidates.poll();
            int v = c.v;
            int d = c.d;

            //意味の無い情報を捨てる
            if(dist[v] < d) {
                continue;
            }

            //確定した頂点から伸びている各辺についてみていく
            List<Edge> edges = G.get(v);
            for(Edge e : edges) {
                if(dist[e.to] > dist[v] + e.cost) {
                    dist[e.to] = dist[v] + e.cost;
                    candidates.offer(new Candidate(e.to, dist[e.to]));
                }
        }
        }
    }
}

UnionFind、簡単だけど改造するの大変そうだね(他人事)

import java.util.Scanner;

public class UnionFind {
    static int[] par = new int[100000];
    public static void main(String[] args) {
        final Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        for(int i = 0; i <= n; i++) {
            par[i] = i;
        }
        System.out.println();
    }
    static int root(int x) {
        if(par[x]==x) {
            return x;
        } else {
            return par[x] = root(par[x]);
        }
    }
    static boolean same(int x, int y) {
        return root(x)==root(y);
    }
    static void unite(int x, int y) {
        x = root(x);
        y = root(y);
        if(x == y) return;
        par[x] = y;
    }
}

nCr mod(109+7)を計算するやつ、逆弦かなんか使っててThe・数学なので理解する木はない ちょっと洗練されてないね...、木が向いたら手直ししよ

import java.util.Scanner;

public class nCr {
    public static void main(String[] args) {
        final Scanner sc = new Scanner(System.in);
        long n = sc.nextLong();
        long r = sc.nextLong();
        //nCr = n!/r!(n-r)! = n ~ r / (n-r)!
        long n_r = n - r;
        System.out.println(String.format("n:%d\nr:%d\nn-r:%d", n, r, n_r));
        long a = 1, b = 1;
        int mod = 1000000007;
        for(int i = 1; i <= n_r; i++) {
            a = a * (r + i) % mod;
            b = b * i % mod;
        }
        long ans = a * func(b, mod - 2, mod) % mod;
        System.out.println(ans);
    }
    static long func(long a, long b, int mod) {
        if(b == 0) {
            return 1;
        }
        if(b % 2 == 0) {
            return func(a * a % mod, b / 2, mod);
        } else {
            return a * func(a, b-1, mod) % mod;
        }
    }
}

最小公倍数、最大公約数を求めるやつ。コード短いのすごいよね

public class lcm_gcd {
    static int gcd(int a, int b) {//最大公約数を求める
        if(b == 0) return a;
        return gcd(b, a%b);
    }
    static int lcm(int a, int b) {//最小公倍数を求める
        int g = gcd(a, b);//まず最大公約数を求める
        return a / g * b;//ユークリッドの互除法により
        // a*b = (aとbの最小公倍数) * (aとbの最大公約数)
        //式変形して  a / (aとbの最大公約数) * b = (aとbの最小公倍数)
        //これで最小公倍数が求まる
    }
}

エラトステネスの篩。この名前覚えられない

import java.util.Scanner;

public class エラトステネスの篩 {
    public static void main(String[] args) {
        final Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();//入力された数までの素数を判定
        int wantUpper = n;
        int furui[] = new int[wantUpper+1];

        for(int i = 2; i <= wantUpper; i++) {
            furui[i]=i;//篩に2~nまでの値を入れる
        }
        for(int i = 2; i <= Math.sqrt(wantUpper); i++) {
            for(int j = i+i; j <= wantUpper; j+=i) {//iの倍数は篩落とす
                furui[j]=0;
            }
        }
        //篩の0ではない値が全て素数
    }
}

ab (mod p)、一生使わないだろうね(数学すぎる)

static long func(long a, long b, int mod) {
    if(b == 0) return 1;
    if(b % 2 == 0) return func(a * a % mod, b / 2, mod);
    else return a * func(a, b - 1, mod) % mod;
}

stackでのdfs、コードじゃなく雰囲気を書きます コードは2次元平面上でのdfsですが、基本的にコメントのように実装するとグラフでも動く

ArrayDeque<Point> deq = new ArrayDeque<>();
deq.push(start);//キューに突っ込む
while(deq.size()!=0) {//キューが空になるまで
    Point p = deq.poll();//まず先頭取り出し
    if(p.equals(goal)) {//終了条件チェック
        System.out.println("Yes");
        return;
    }
    if(p.x >= h || p.x < 0 || p.y >= w || p.y < 0) {//異常チェック
        continue;
    }
    if(map[p.x][p.y] == '#') {//異常チェック
        continue;
    }
    map[p.x][p.y] = '#';//通ったという記録
    for(int i = 0; i < 4; i++) {
        deq.push(new Point(p.x + x[i], p.y + y[i]));//他のところへ遷移したのをキューに突っ込む
    }
    //モノによってはここで通ったという記録を消す
}
System.out.println("No");

bit全探索、実装結構軽いよね

int ketasu = 4;//桁の数 4なら0000 ~ 1111まで
for(int i = 0; i < (1<<ketasu); i++) {

    for(int j = ketasu-1, k = 0; j >= 0; j--, k++) {
        if(((i>>j) & 1) == 1) {//左からj番目のbitが立っている(1)かどうか

        }
    }

    for(int j = 0; j < ketasu; j++) {
        if(((i>>j) & 1) == 1) {//右からj番目のbitが立っている(1)かどうか

        }
    }
            
}

Javaの修飾子、strictfpとは?

はじめまして!naoppyです。

初投稿なんですけど、ブログは備忘録として淡々と書いていこうと思います。

 

今回はstrictfpについてです。

strictfpとは?

strictfpとは、Javaの修飾子です。

stricffp修飾子は、32bitや64bitによって浮動小数点演算の誤差が違う計算を、どの環境でも同じ結果にするための演算子です。

簡単に言うと、誤差の誤差を無くす修飾子です。

大切なのは、浮動小数点演算の誤差が無くなるわけではないです。

Javaの設計理念である、"どこでも同じように動く"、という目的のためでしょうが、あまり出番はないです。

僕はこれを厳密計算の為の修飾子で、つけたらBigDecimalを使わなくてもいいのかと期待してました・・・悲しいね。

結局、厳密な少数計算にはBigDecimalクラス

BigDecimal (Java Platform SE 8)

を使う必要がありますね。

コード例

この修飾子はクラス、メソッドに対して付けることができます。

はてブロでのソースコードの貼り方がわからない…

strictfp class Test1 {
    void test() {
    //以下省略~
    }
}

class Test2 {
    strictfp void test() {
    //以下省略~
    }
}

 

まとめ

  •  誤差の誤差をなくす修飾子
  • 浮動小数点演算の誤差をなくすことはできない
  • 現在ではあまり使われていない
  • クラスとメソッドにつけることができる