シャンペーンの噴水パズル


30

空のグラスは次の順序で並べられます。

ここに画像の説明を入力してください

液体がいっぱいの場合、1枚目のガラスに液体を注ぐと、余分な液体がガラス2と3に等量で流れます。ガラス2がいっぱいになると、余分な液体が4と5などに流れます。

液体がNリットルで、各ガラスの最大容量が1リットルである場合getWaterInBucket(int N, int X)、Xがガラス数である関数に記入してガラスに注ぐことにより液体をNリットル空けた場合、ガラスに存在する液体の量を与えます。たとえば、最初に4リットルが必要で、ガラス3の水を見つけたい場合、関数はgetWaterInBucket(4, 3)

プログラムでこれを解決するにはどうすればよいですか?パスカルの三角形を使って数学の解を見つけようとしました。これは機能しませんでした。私はそれをツリーだと考えたので、このようなパラメーターを追加し、getWaterInBucket(BTree root, int N, int X)各レベルで再帰的な解決策を試すことができますが、この問題ではパラメーターは許可されていません。明らかな何か、いくつかのトリックはありますか?


18
経営者の問題がシャンパンの噴水に関するものである会社で働きたくないのですが…
-mouviciel

ガラス1以外のガラスに注ぐことはできますか?そうでない場合、各層の各ガラスには同じ量の水が含まれます。したがって、1、3、6、10 ...リットルを注ぐたびに、完全な層ができます。7リットルを注ぐと、4列目に4つのグラスがあり、それぞれが1/4満杯になります。その上のすべての層がいっぱいになります。
グレンペターソン

5
@GlenPeterson私がそれを読んでいる方法から、私は彼らが等しく満たすとは思わない。はい、2と3は同じように満たされます。なぜなら、1つだけが注がれるからです。しかし、いっぱいになると4/5に2つ、5/6に3つが等しくなり、4/6のラットの2倍で5が満たされます。 。中央のカップは常に外側のカップよりも速く充填されます。4/6がいっぱいになるまでに8/9は25%いっぱいになり、7/10はまだ空です。
ブラッド

1
また、これはパスカルの三角形を思い出させます
ブラッド

@mouviciel Haha GlenPeterson-最初に注がれるグラスは常にグラス1です。インタビュアーはまた、この情報を使用すると言った。彼は私がこの問題に対する正しい答えについていたよりも混乱しているようでした。
-Slartibartfast

回答:


35

あなただけのような、注ぐをシミュレートする必要があります

void pour(double glasses[10], int glass, double quantity)
{
    glasses[glass] += quantity;
    if(glasses[glass] > 1.0)
    {
         double extra = glasses[glass] - 1.0;
         pour( glasses, left_glass(glass), extra / 2 );
         pour( glasses, right_glass(glass), extra / 2 );
         glasses[glass] = 1.0;
    }
}

double getWaterInGlass(int N, int X)
{
    double glasses[10] = {0,0,0,0,0,0};
    pour(glasses, 0, X);
    return glasses[N];
}

現状では、これは木ではありません。異なるグラスが同じグラスに注がれるため、それがツリーになるのを防ぎます。


16
これは木ではないという素晴らしい観察のために+1。
ミハイダニラ

2
いい答えだ。インタビューでは、すべてのメガネの内容を計算するため、スケーラビリティの問題が発生する可能性があると言う必要があります。また、グラスの一番下の列から水が流れる場合にも対処する必要があります。そして、あなたがしたいreturn glasses[N-1]ガラス番号は1ではなく0から始まりますので、
トム・パン

1
難しい部分は、左と右の子供のインデックスを把握することだと思います。これを提示した場合、インタビュアーはそれらの機能を実装するように求めます。明示的な式がある場合があります。
ジェームズ

それは本当にエレガントなソリューションです。ありがとう。コード行にコメントを追加して、思考プロセスで各ステップが何を意味するかを説明できるように編集していただければ幸いです。また、メガネの数は10個に限定されません。何でも
構い

1
左右のメガネはどうやって見つけますか?
キューウィック

7

インタビューの状況でこの質問に答える方法は次のとおりです(この質問を見たことがなく、解決策が見つかるまで他の答えを見ませんでした)。

最初に、私はそれを理解しようとしました(これを「数学の解法」と呼びます)。グラス8に到達したとき、グラス4の前にグラス5があふれ始めるため、見た目よりも厳しいことに気付きました。再帰ルートに進むことにしました(参考までに、多くのプログラミングインタビューの質問では、解決するために再帰または帰納法が必要です)。

再帰的に考えると、問題ははるかに簡単になります。ガラス8にどれくらいの水が含まれていますか?グラス4と5からこぼれた量の半分(いっぱいになるまで)。もちろん、それはメガネ4と5からどれだけ流出したかを答えなければならないことを意味しますが、それもそれほど難しくないことがわかります。ガラス5からどれだけこぼれましたか?しかし、ガラス2と3から多くがこぼれた半分から、ガラス5に残ったリットルを差し引いた。

これを完全に(そして乱雑に)解決すると:

#include <iostream>
#include <cmath>
using namespace std;

double howMuchSpilledOutOf(int liters, int bucketId) {
    double spilledInto = 0.0;
    switch (bucketId) {
        case 1:
            spilledInto = liters; break;
        case 2:
            spilledInto = 0.5 * howMuchSpilledOutOf(liters, 1); break;
        case 3:
            spilledInto = 0.5 * howMuchSpilledOutOf(liters, 1); break;
        case 4:
            spilledInto = 0.5 * howMuchSpilledOutOf(liters, 2); break;
        case 5:
            spilledInto = 0.5 * howMuchSpilledOutOf(liters, 2) + 0.5 * howMuchSpilledOutOf(liters, 3); break;
        case 6:
            spilledInto = 0.5 * howMuchSpilledOutOf(liters, 3); break;
        default:
            cerr << "Invalid spill bucket ID " << bucketId << endl;
    }
    return max(0.0, spilledInto - 1.0);
}

double getWaterInBucket(int liters, int bucketId) {
    double contents = 0.0;
    switch (bucketId) {
        case 1:
            contents = liters; break;
        case 2:
            contents = 0.5 * howMuchSpilledOutOf(liters, 1); break;
        case 3:
            contents = 0.5 * howMuchSpilledOutOf(liters, 1); break;
        case 4:
            contents = 0.5 * howMuchSpilledOutOf(liters, 2); break;
        case 5:
            contents = 0.5 * howMuchSpilledOutOf(liters, 2) + 0.5 * howMuchSpilledOutOf(liters, 3); break;
        case 6:
            contents = 0.5 * howMuchSpilledOutOf(liters, 3); break;
        case 7:
            contents = 0.5 * howMuchSpilledOutOf(liters, 4); break;
        case 8:
            contents = 0.5 * howMuchSpilledOutOf(liters, 4) + 0.5 * howMuchSpilledOutOf(liters, 5); break;
        case 9:
            contents = 0.5 * howMuchSpilledOutOf(liters, 5) + 0.5 * howMuchSpilledOutOf(liters, 6); break;
        case 10:
            contents = 0.5 * howMuchSpilledOutOf(liters, 6); break;
        default:
            cerr << "Invalid contents bucket ID" << bucketId << endl;
    }
    return min(1.0, contents);
}

int main(int argc, char** argv)
{
    if (argc == 3) {
        int liters = atoi(argv[1]);
        int bucket = atoi(argv[2]);
        cout << getWaterInBucket(liters, bucket) << endl;
    }
    return 0;
}

この時点で(またはこれを書いているときに)、インタビュアーに、これは本番環境での理想的なソリューションではないことを伝えます。howMuchSpilledOutOf()との間にコードが重複していますgetWaterInBucket()。バケットを「フィーダー」にマッピングする中央の場所が必要です。しかし、実装の速度と精度が実行の速度と保守性よりも重要であるインタビュー(特に明記されていない限り)では、このソリューションが望ましいです。次に、コードをリファクタリングして、プロダクション品質と考えるものに近づけ、インタビュアーに決定させます。

最後の注意:私のコードにはどこかにタイプミスがあると確信しています。インタビュアーにもそれについて言及し、リファクタリングまたは単体テストを行った後、より自信を持つようになると言います。


6
このソリューションは、例のためにハードコーディングされています。メガネを追加するということは、スイッチに「ケース」を追加することを意味します...良い解決策だとは思いません。
ルイージマッサガレラノ

2
@LuigiMassaGallerano-インタビューの質問なので、この場合は大丈夫です。それは完璧な解決策ではありません。インタビュアーは、候補者の思考プロセスをよりよく理解しようとしています。そして、トムはすでにそれを指摘していthis isn't the ideal solutionます。

1
正直そうではありません。このシナリオはハードコーディングすることを意図したものではないことを保証できます。インタビューの質問をして、インタビュー対象者がハードコーディングされたソリューションを提示するテストケースシナリオをレイアウトした場合、一般的なソリューションを提供する準備ができているか、おそらくインタビューに合格しません。
リグ

5

これをツリーの問題として考えると、それは赤いニシンであり、実際には有向グラフです。しかし、それについてはすべて忘れてください。

一番上のガラスの下にあるガラスを考えてください。その上に1つまたは2つのグラスがあり、そこにあふれることがあります。座標系を適切に選択すれば(心配する必要はありません。最後を参照)、任意のグラスの「親」グラスを取得する関数を作成できます。

これで、ガラスからのオーバーフローに関係なく、ガラスに注がれた液体の量を取得するアルゴリズムを考えることができます。しかし、答えは、多くの液体が各親に注がれ、各親グラスに保存されている量を2で割ったものです。amount_poured_into()関数の本体のpythonフラグメントとしてこれを書く:

# p is coords of the current glass
amount_in = 0
for pp in parents(p):
    amount_in += max((amount_poured_into(total, pp) - 1.0)/2, 0)

max()は、負のオーバーフローが発生しないようにするためのものです。

ほぼ完成です!ページの下に「y」、1行目が0、2行目が1などの座標系を選択します。「x」座標は最上行のガラスの下にゼロを持ち、2行目は-1のx座標を持ち、 +1、3番目の行-2、0、+ 2など。重要な点は、レベルyの左または右端のガラスがabs(x)= yになることです。

これらすべてをpython(2.x)にまとめると、次のようになります。

def parents(p):
    """Get parents of glass at p"""

    (x, y) = p
    py = y - 1          # parent y
    ppx = x + 1         # right parent x
    pmx = x - 1         # left parent x

    if abs(ppx) > py:
        return ((pmx,py),)
    if abs(pmx) > py:
        return ((ppx,py),)
    return ((pmx,py), (ppx,py))

def amount_poured_into(total, p):
    """Amount of fluid poured into glass 'p'"""

    (x, y) = p
    if y == 0:    # ie, is this the top glass?
        return total

    amount_in = 0
    for pp in parents(p):
        amount_in += max((amount_poured_into(total, pp) - 1.0)/2, 0)

    return amount_in

def amount_in(total, p):
    """Amount of fluid left in glass p"""

    return min(amount_poured_into(total, p), 1)

そのため、実際にガラスの量をpで取得するには、amount_in(total、p)を使用します。

OPからは明らかではありませんが、「パラメーターを追加できない」ということは、表示されているガラスのに関して元の質問に答えなければならないことを意味する場合があります。これは、ショーガラス番号から上記で使用した内部座標系へのマッピング関数を記述することで解決されます。面倒ですが、反復解法または数学解法のいずれかを使用できます。わかりやすい反復関数:

def p_from_n(n):
    """Get internal coords from glass 'number'"""

    for (y, width) in enumerate(xrange(1, n+1)):
        if n > width:
            n -= width
        else:
            x = -y + 2*(n-1)
            return (x, y)

ここで、ガラスの数を受け入れるように上記のamount_in()関数を書き換えます。

def amount_in(total, n):
    """Amount of fluid left in glass number n"""

    p = p_from_n(n)
    return min(amount_poured_into(total, p), 1)

2

面白い。

これはシミュレーションのアプローチを取ります。

private void test() {
  double litres = 6;
  for ( int i = 1; i < 19; i++ ) {
    System.out.println("Water in glass "+i+" = "+getWater(litres, i));
  }
}

private double getWater(double litres, int whichGlass) {
  // Don't need more glasses than that.
  /*
   * NB: My glasses are numbered from 0.
   */
  double[] glasses = new double[whichGlass];
  // Pour the water in.
  pour(litres, glasses, 0);
  // Pull out the glass amount.
  return glasses[whichGlass-1];
}

// Simple non-math calculator for which glass to overflow into.
// Each glass overflows into this one and the one after.
// Only covers up to 10 glasses (0 - 9).
int[] overflowsInto = 
{1, 
 3, 4, 
 6, 7, 8, 
 10, 11, 12, 13, 
 15, 16, 17, 18, 19};

private void pour(double litres, double[] glasses, int which) {
  // Don't care about later glasses.
  if ( which < glasses.length ) {
    // Pour up to 1 litre in this glass.
    glasses[which] += litres;
    // How much overflow.
    double overflow = glasses[which] - 1;
    if ( overflow > 0 ) {
      // Remove the overflow.
      glasses[which] -= overflow;
      // Split between two.
      pour(overflow / 2, glasses, overflowsInto[which]);
      pour(overflow / 2, glasses, overflowsInto[which]+1);
    }
  }
}

どの印刷(6リットル):

Water in glass 1 = 1.0
Water in glass 2 = 1.0
Water in glass 3 = 1.0
Water in glass 4 = 0.75
Water in glass 5 = 1.0
Water in glass 6 = 0.75
Water in glass 7 = 0.0
Water in glass 8 = 0.25
Water in glass 9 = 0.25
Water in glass 10 = 0.0
...

ほぼ正しいようです。


-1

これは二項関数です。レベルNのガラス間の水の比率は、レベル内の各ガラスのnCrを使用して検出できます。さらに、レベルNより前の眼鏡の総数は1から(N-1)の合計であり、これはかなり簡単に入手できる式です。したがって、Xが与えられると、そのレベルを判断し、nCrを使用してそのレベルのグラスの比率を確認し、Xに到達するのに十分なリットルがある場合、Xにどれだけの水があるかを判断できるはずです。

第二に、BTreeを使用するというアイデアは素晴らしいです。BTreeは外部パラメーターではなく、内部変数であるというだけです。

IOW、もしあなたがあなたの教育でこの数学をカバーしていたら(ここ英国では大学の前に教えられています)、あまり問題なくこれを解決できるはずです。


1
二項関数だとは思わない。二項関数が示唆するように、1,2,1の割合で3番目のレベルに達しますが、最初に中央のガラスがいっぱいになり、その後パターンが壊れます。
ウィンストンユワート

時間はシミュレーションの一部ではなく、最終結果には影響しません。
DeadMG

4
モデリング液がグラスに満たされ、グラスから流出するため、その時間を暗黙的にシミュレーションの一部として維持する必要があります。5リットルでは、4と6は半分いっぱいになり、5はすべていっぱいになります。6リットルが追加されると、8と9に注ぎ始めますが、4と6はまだ容量に達していないため、7と10は水を受け取りません。したがって、二項関数は正しい値を予測しません。
ウィンストンイーバート

3
-1、これは間違っています。レベルは均一に塗りつぶされません。
dan_waterworth

あなたは正しい、私はそれを考慮しませんでした。しかし、しばらくそれについて考えた後、私はあなたが正しかったことに気づきました。これを考慮して式を調整する方法がわからない。
DeadMG
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.