ポイントが10あるとします
あるものの価値が、1ポイントを使うことによって
A(後)=A(前)×(100+p)/100(小数点以下切り捨て)にグレードアップできるとします。
ここでAはその物の価値、pはポイントです。
この時Aを最大にするにはどのようにポイントを使っていけば良いか、という問題ですが
私がこれを解こうとした時に考えた事は
まずは足して10になる組み合わせを全て列挙してリスト化すると言うものですが、まず、それを列挙するアルゴリズムが思いつかないですし(フィボナッチ数的にできるかもとか考えましたけど全然違いました)
小数点以下切り捨ての問題もあるので単純な組み合わせではなく順列で列挙しないといけないのでは無いかと考えました。
今回はポイントが10なのでいいですが、100とかになると仮にそのアルゴリズムを思いついたとしても膨大な数になるのは目に見えています。
ポイントを使う回数も決められていないので、単純に足して10になるポイントの使い方を考えただけでも相当大きくなると思いますし列挙するのも簡単ではないと思います。
上記の問題は、Aの最大値を求めよと書いてあったので確かに答えは一意に求まる物のはずです。しかし組み合わせを全て列挙しないと最適解は求められないのでは?と言うのが私の疑問です。
どのように考えたらいいのでしょうか?よろしくお願いします。
ちなみにですけど、ソルバーなどのライブラリを使って解くという方法もあるかもしれないですが、きちんとアルゴリズムとしてその問題を解きたいです。
よろしくお願いします。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答6件
0
動的計画法に基づくアルゴリズムだと、初期ポイントをp_0
として(p_0 + 1) * (p_0 / 2)
回の計算で解を求めることができます。以下で、動的計画法に基づくアルゴリズムの考え方を説明します。
例えば、初期価値をA_0 = 100
、初期ポイントをp_0 = 10
の場合の問題を考えましょう。この問題をProblem(100, 10)と書くことにします。
最後に何ポイント使うかによって場合分けすると、以下の10通り考えることができます。
ケース1. 最後に1ポイントだけ使う場合、直前には9ポイント残っている
ケース2. 最後に2ポイント使う場合、直前には8ポイント残っている
...
ケース10. 最後に10ポイント全部使う場合、直前には0ポイント残っている
実はこれらの問題は、元の問題の部分問題の解を用いて上手く解くことができます。どういうことか見ていきましょう。
例えば、ケース2の場合の最適解は、直前の8ポイントの最適な使い方がわかっていれば実は簡単に求められます。つまり、初期価値をA_0 = 100
、初期ポイントをp_0 = 8
とした問題Problem(100, 8)の解がわかっていれば、ケース2の場合の最適解は、
ケース2の最適解 = Problem(100, 8)の最大価値 * (100 + 2) / 100(小数点以下切り捨て)
として求められます。
同様に、ケース1からケース10までの解はそれぞれ、
ケース1の最適解 = Problem(100, 9)の最大価値 * (100 + 1) / 100(小数点以下切り捨て)
ケース2の最適解 = Problem(100, 8)の最大価値 * (100 + 2) / 100(小数点以下切り捨て)
...
ケース10の最適解 = Problem(100, 0)の最大価値 * (100 + 10) / 100(小数点以下切り捨て)
となります。
ここで、ケース10の最適解で現れるProblem(100, 0)の最大価値は、明らかに100であることに注意してください。なぜなら、Problem(100, 0)は初期価値100で与えられたポイントが0だからです。
元の問題Problem(100, 10)の解は、ケース1からケース10の最適解のうち最も大きい価値を取るものになります。
このように、元の問題はより少ないポイントしか使えない状況下での最適解(部分問題の最適解)を用いて容易に計算できます。これを素朴に再帰を用いて実装したコードが一番下に書いてあるコードになります。
一方で、再帰を用いずにループだけで実装することも可能です。ループだけで実装する場合は、次のように考えるとよいです。
まず、Problem(100, 0)の解は上でも述べたように100であることは自明ですね。
次に、Problem(100, 1)の解はProblem(100, 0)の最大価値 * (100 + 1) / 100(小数点以下切り捨て)で計算できます。
次に、Problem(100, 2)の解は
最後に1ポイント使う場合: Problem(100, 1)の最大価値 * (100 + 1) / 100(小数点以下切り捨て)
最後に2ポイント使う場合: Problem(100, 0)の最大価値 * (100 + 2) / 100(小数点以下切り捨て)
のうち価値がより大きくなる方です。ただし、Problem(100, 1)の最大価値とProblem(100, 0)の最大価値はすでに求まっていることに注意してください。
次に、Problem(100, 3)の解は
最後に1ポイント使う場合: Problem(100, 2)の最大価値 * (100 + 1) / 100(小数点以下切り捨て)
最後に2ポイント使う場合: Problem(100, 1)の最大価値 * (100 + 2) / 100(小数点以下切り捨て)
最後に3ポイント使う場合: Problem(100, 0)の最大価値 * (100 + 3) / 100(小数点以下切り捨て)
のうち価値が最も大きくなるものです。ただし、Problem(100, 2)の最大価値とProblem(100, 1)の最大価値とProblem(100, 0)の最大価値はすでに求まっていることに注意してください。
このように与えられたポイントが小さい部分問題から順に解を求めていくことで単純なループで元の問題の解を求めることが可能です。
ループのみ再帰なしの実装を下に挙げます。
必要な計算回数はそれぞれ次のようになります。
Problem(100, 1): 1回
Problem(100, 2): 2回
Problem(100, 3): 3回
...
Problem(100, 10): 10回
なので、トータル(1 + 10) * (10 / 2) = 55回の計算で元の問題の解を求めることが可能です。
このように動的計画法では部分問題の解についての漸化式を用いて、より少ない計算量で元の問題の解を求めることができます。今回の問題においては、部分問題の解について以下の漸化式が成立していると解釈することができます。
Problem(A_0, p_0) = max_(p=1, ..., p_0) Problem(A_0, p_0 - p)の最大価値 * (100 + p) / 100(小数点以下切り捨て)
python
1A_0 = 100 2p_0 = 10 3 4 5def solve(A_0, p_0): 6 # Subproblem of this problem is defined by A_0 and p_0, each corresponds to 7 # the initial value of some object and the points given initially. 8 # Variable `solutions` holds the optimal values of subproblems as a dictionary. 9 # The solution of the subproblem with (A_0, 0) is apparent, that is A_0. 10 solutions = {(A_0, 0): A_0} 11 for p_0 in range(1, p_0 + 1): 12 # Solve the subproblem with (A_0, p_0), p_0 begins with 1 13 # and ends with p_0 given to the function `solve`. 14 max_A = A_0 15 for point_to_be_used in range(1, p_0 + 1): 16 # case division with the points to be used in the last time. 17 A = solutions[(A_0, p_0 - point_to_be_used)] * (100 + point_to_be_used) // 100 18 if A > max_A: 19 max_A = A 20 # add solution of the subproblem with (A_0, p_0) 21 solutions.update({(A_0, p_0): max_A}) 22 return solutions[(A_0, p_0)] 23 24print(solve(A_0, p_0))
以下、更新前の再帰に基づく実装です。
kichirb3さんの「小数点以下切り捨て」についての指摘を受けて改めて考え直した結果、解析的に閉じた形で解を求めることが難しいと思いました。
しかし、2^Nより効率の良いアルゴリズムを実装することは可能です。
動的計画法に基づいてより効率の良いアルゴリズムを実装しました。
今少し時間がないので取り急ぎ実装のみ上げておきます。
A_0やp_0を大きくして実行しても非常に高速に実行可能で、スケールします。
ただし、p_0をかなり大きくした場合には、Maximum Recursion Error(最大再帰呼び出し数の上限を超える)が発生しますが、pythonの再帰の上限を変更すればエラーは起こりません。
説明や計算量や補足については後ほど時間があるときに追記します。
計算量はちゃんと確認していませんが、多分O(p_0^2)です。計算時間がp_0^2にだいたい比例するということです。
python
1from math import floor 2 3original_A_0 = 100 4original_p_0 = 10 5solutions = {} 6 7def formula(A, p): 8 assert isinstance(A, int) 9 assert isinstance(p, int) 10 return A + floor(A * p / 100) 11 12 13def solve(A_0, p_0): 14 '''Return the maximum objective value of the problem with A_0 and p_0.''' 15 assert isinstance(A_0, int) 16 assert isinstance(p_0, int) 17 if p_0 == 0: 18 # if no points left, register solution and return back 19 solutions.update({(A_0, p_0): A_0}) 20 return A_0 21 if (A_0, p_0) in solutions: 22 # if solution is already known, register solution and return back 23 return solutions[(A_0, p_0)] 24 25 maximum = A_0 26 for p in range(1, p_0 + 1): 27 A_previous = solve(A_0, p_0 - p) 28 A_current = formula(A_previous, p) 29 if A_current > maximum: 30 maximum = A_current 31 # register solution and return back 32 solutions.update({(A_0, p_0): maximum}) 33 return maximum 34 35maximum = solve(original_A_0, original_p_0) 36print(maximum)
投稿2018/11/08 01:01
編集2018/11/15 00:52総合スコア229
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/11/08 01:46
2018/11/08 02:00
2018/11/08 02:51
2018/11/08 13:27
2018/11/08 16:54
2018/11/15 00:44
0
まず、Aの初期値が正値の場合、例示された漸化式なら常に1ポイントずつ利用していくのが解となります(切り捨て操作が入るけど結果に影響ないだろう)
すみません。初期値A=70などでは切り捨て影響して最適解ではなくなりますね。 2018/11/08 08:52修正
理由は説明できませんが、複利で効いてくるローンなどの利率計算と同じと考えてよいと思います。
~~しかしもし漸化式がもっと複雑だったり単調増加しないものであれば、~~ご推察のとおり全探索をする必要がでてきます。
その場合、ちょっと紙に書いて数え上げれば分かる通り、2^(N-1)通りの組み合わせが存在します。ここでN=ポイント数です。
この組み合わせの素朴な探索方法、すなわち解き方としては深さ優先探索というアルゴリズムが適しています。
これは「まずは足して10になる組み合わせを全て列挙」とは逆の発想で
「1~残ポイント数ずつひいていって0ポイントになるまで次の値を求めていく」という考え方をします。
たとえば以下のようなコードになります。
Online Python Tutorなどで実際に動かして動作を追ってみてください。
Python
1import math 2 3# 次の値を返す 4def next(A,p): 5 return math.floor(A*(100+p)/100) 6 7# (たぶん)厳密解 8N = 10 # 利用可能な総ポイント数 9A = 10000 10for _ in range(N): 11 A = next(A,1) 12print('厳密解:',A) # 11045 13 14# 探索 15def search(A,remain,ret): # 現在値, 残ポイント, 結果(最大値) 16 # 残ポイントなしなら最大値を記録 17 if remain <= 0: 18 if A > ret[0]: 19 ret[0] = A 20 return 21 # 1...残ポイントずつ使って次の値を求める 22 for p in range(1,remain+1): 23 An = next(A,p) 24 search(An,remain-p,ret) # 次の値を元に再帰で探索。残ポイントは現在の利用ポイント分を引く 25 26A,ret = 10000,[-1] # 初期値と結果(最大値が格納される) 27search(A,N,ret) 28print('探索解:',ret[0]) # 11045
ただし実際に実行させてみると分かりますが、N=20を超えると実質時間がかかりすぎて解が求められなくなります。
速度アップするには順列生成をitertoolsに任せるなどの工夫が必要になってくるかと思います。
投稿2018/11/07 22:12
編集2018/11/08 01:57総合スコア38262
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/11/07 22:48
2018/11/07 23:02
2018/11/07 23:03
2018/11/07 23:07
2018/11/07 23:11
2018/11/07 23:12
2018/11/07 23:17
2018/11/07 23:24
2018/11/07 23:40
2018/11/07 23:50
2018/11/08 01:36
2018/11/08 01:55
2018/11/08 02:00
2018/11/08 02:00
2018/11/08 02:10 編集
2018/11/08 06:18
2018/11/08 06:39
2018/11/08 06:47
2019/02/02 22:30
2019/02/03 01:06
2019/02/05 00:46
2019/02/05 01:16
2019/02/05 01:48
0
ヒープキューに格納する際、残りポイント数を記録する必要はなさそうなので、Aの値(を-1倍したもの)のみを格納するようにしました。
python
1from heapq import heappush, heappop 2 3def solveA(A, point): 4 status = [[-A] for _ in range(point + 1)] 5 for i in range(point): 6 A = heappop(status[i]) 7 for p in range(1, point - i + 1): 8 heappush(status[i + p], (-(-A * (100 + p) // 100))) 9 return -heappop(status[point])
以下は以前の解答です。
heapqを使って解いてみました。
python
1from heapq import heappush, heappop 2 3def solveA(A, point): 4 status = [[] for _ in range(point + 1)] # iポイント使った時の結果(Aと残りポイント)を格納するヒープキュー 5 status[0].append((-A, point)) # 0ポイント消費時の(符号を逆にしたA, 残りポイント) を初期値として格納 6 7 for i in range(point): 8 A, remaining_point = heappop(status[i]) if status[i] else (0, 0, []) # 同じiポイント使ったとき、Aの値が一番大きくなるケースをピックアップ 9 A *= -1 # Aの符号を戻す 10 for p in range(1, remaining_point + 1): 11 heappush(status[i + p], (-(A * (100 + p) // 100), remaining_point-p)) 12 13 A, _ = heappop(status[point]) 14 return -A
投稿2018/11/08 07:01
編集2018/11/08 10:14退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/11/08 08:06 編集
0
上記の問題は、Aの最大値を求めよと書いてあったので確かに答えは一意に求まる物のはずです。しかし組み合わせを全て列挙しないと最適解は求められないのでは?と言うのが私の疑問です。
can110さんのおっしゃる通り、任意の初期ポイントに対して最適解は常にポイントを1ポイントずつ使った場合になります。
ポイントを使う回数も決められていないので、単純に足して10になるポイントの使い方を考えただけでも相当大きくなると思いますし列挙するのも簡単ではないと思います。
おっしゃる通り、列挙アルゴリズムはややこしいです。ちなみにこの手の数え上げは場合の数のみを考える場合でさえ、効率の良い方法はないと思います。
以下に、最適解がなぜ1ポイントずつ使った場合になるのか説明します。
まず、初期ポイントが2ポイントである場合を考えましょう。ポイントの使い方は2通りあります。初期価値をv_0、最終価値をv_fとすると、それぞれ、
v_f_1 = v_0 * {(100 + 1) / 100} ^ 2 = v_0 * {1 + 1/100} ^ 2 = v_0 * {1 + 2/100 + ...} v_f_2 = v_0 * {(100 + 2) / 100} = v_0 * {1 + 2/100}
と書けます。どちらが大きいかを比べるためにv_f_1
を展開しました。1ポイントずつ使った方が大きくなりますね。
次に、初期ポイントが3ポイントである場合を考えましょう。ポイントの使い方は2通りあります。1ポイントを3回に分けて使う場合と、1ポイントを1回と2ポイントを1回使う場合です。
v_f_1 = v_0 * {(100 + 1) / 100} ^ 3 = v_0 * {1 + 1/100} ^ 3 = v_0 * {1 + 3/100 + ...} v_f_2 = v_0 * {(100 + 3) / 100} = v_0 * {1 + 3/100}
と書けます。どちらが大きいかを比べるためにv_f_1
を展開しました。1ポイントずつ使った方が大きくなりますね。
もうわかったかもしれませんが、一般にNポイントを1回で使うよりも1ポイントをN回使う方が大きくなります。これは二項定理よりすぐに成り立ちます。
さらに例えば、初期ポイントが10のとき、1ポイントを10回使った場合と(1, 2, 3, 4)の順で使った場合を比べると、
v_f_1 = v_0*{1 + 1/100} ^ 10 = v_0*{1 + 1/100} + v_0*{1 + 1/100}^2+ v_0*{1 + 1/100}^3+ v_0*{1 + 1/100}^4 v_f_2 = v_0*{1 + 1/100} + v_0*{1 + 2/100} + v_0*{1 + 3/100} + v_0*{1 + 4/100}
となり、各項ごとに縦に比較すると、v_f_1の方が大きい方ことがわかります。
このように初期ポイントPを任意の分け方で分けた場合は、ポイントを1ポイントずつに分けた場合に比べて常に小さくなることがわかるはずです。
なので、任意のポイントの使い方のうち1ポイントずつ順に使う方法が価値を最大化するというわけです。
投稿2018/11/07 22:38
総合スコア229
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2018/11/07 23:40
2018/11/07 23:45
2018/11/07 23:56
0
追記2: Takahito_Ogawaさんにコメントいただき考えてみるとなるほど再帰的に最適解を求める考え方でよく、一度求めたa, pに関する最適解を複数回計算しなくてよいような論理が容易に組めることに気づけました。コメントありがとうございました。>Takahito_Ogawaさん
既にコードは複数上がっていて本質的には同じことだと思いますが、自分なりの「分かり易い」コードを書いてみました。例えばkichirb3さんのコードは再帰を必要としないコードですが、悲しいかなパっと見てすぐに腑に落ちなかった(^^;)ため自分なりに書いてみたわけです。
Python
1import math 2 3debug_count = 0 # fを何回評価したかの計算量評価用カウンター 4 5def f(a, p): 6 global debug_count 7 debug_count += 1 8 return int(math.floor(a * (100 + p) / 100)) 9 10def solve(a, p): 11 assert p > 0 12 best = {} 13 _count = debug_count 14 15 def inner(p): 16 # 既に(a, p)の最大値が計算済みなら単にそれを返す 17 if p in best: 18 return best[p] 19 # 最初に全てのポイントを一度につぎ込んだ場合を計算 20 max_a, max_ps = f(a, p), [p] 21 for i in range(1, p): 22 # p - iポイントをつぎ込んでからiポイントをつぎ込んだ場合を計算 23 # まずp - iポイントをつぎ込んだ場合の最大値・組み合わせを再帰で求め>る 24 pre_a, pre_ps = inner(p - i) 25 # その後iポイントをつぎ込んだ最終結果を求める 26 can_a = f(pre_a, i) 27 # 最大値の更新 28 if can_a > max_a: 29 max_a, max_ps = can_a, pre_ps + [i] 30 best[p] = max_a, max_ps 31 return best[p] 32 33 res = inner(p) 34 return res + (debug_count - _count,) 35 36print(solve(70, 10)) # (77, [10], 55) 37print(solve(10000, 10)) # (11045, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 55)
追記:Takahito_Ogawaさんの動的計画法をちゃんと理解できてませんが、同じポイントを消費する組み合わせのうち最大のもの以外を候補から手際よく削除しながらベストなものの探索空間を絞り込むというアプローチではないかと思いました。それに対して本回答は劣っていて単にgen_psを示すためだけのものでしかないです。
なおポイント数11の最大値は10のときと同じなので1ポイントは無駄ということになり、無暗に組み合わせが増えているだけでありしらみつぶし探索のよくない特徴が浮き彫りになってますね。動的計画法であればこういう影響を受けず本質的に有効な組み合わせを早く求められるのではないかと思います。
足して10になる組み合わせを全て列挙してリスト化すると言うものですが、まず、それを列挙するアルゴリズムが思いつかない
実際には他の回答でのやりとりにあるように単に組み合わせではなく順列を加味した組み合わせになるのでしょうね・・・
1, 2
2, 1
を異なる組み合わせとみなすということです。リスト化する必要はないので一時に一つの組み合わせが列挙できれるようなiterator(実装ではgenerator)があればよいと思います。それは以下のgen_psのような感じで書けます。ポイントが10程度だとスタックオーバーフローの心配もないので最も単純と思える再帰を用いてます。
Python
1import math 2import functools 3 4 5def f(a, p): 6 return math.floor(a * (100 + p) / 100) 7 8 9def gen_ps(p, ps): 10 for i in range(1, p): 11 yield from gen_ps(p - i, ps + [i]) 12 yield ps + [p] 13 14 15def solve(a0, p): 16 print('initial A:', a0, 'total points:', p) 17 count = 0 18 max_a = a0 19 best_pss = [] 20 for ps in gen_ps(p, []): 21 count += 1 22 a = functools.reduce(f, ps, a0) 23 if a > max_a: 24 # print(a, ps) 25 max_a = a 26 best_pss = [ps] 27 elif a == max_a: 28 best_pss.append(ps) 29 print('maximum value: ', max_a) 30 print('number of best combinations: ', len(best_pss)) 31 for ps in best_pss: 32 print(ps) 33 print('total number of checked candidates: ', count) 34 35 36solve(70, 10) 37solve(70, 11) 38 39# 結果==> 40initial A: 70 total points: 10 41maximum value: 77 42number of best combinations: 2 43[3, 7] 44[10] 45total number of checked candidates: 512 46initial A: 70 total points: 11 47maximum value: 77 48number of best combinations: 27 49[1, 3, 7] 50[1, 10] 51[2, 2, 7] 52[2, 3, 3, 3] 53[2, 3, 6] 54[2, 6, 3] 55[2, 9] 56[3, 1, 7] 57[3, 2, 3, 3] 58[3, 2, 6] 59[3, 3, 2, 3] 60[3, 3, 3, 2] 61[3, 3, 5] 62[3, 5, 3] 63[3, 6, 2] 64[3, 7, 1] 65[3, 8] 66[4, 7] 67[5, 3, 3] 68[5, 6] 69[6, 2, 3] 70[6, 3, 2] 71[6, 5] 72[8, 3] 73[9, 2] 74[10, 1] 75[11] 76
上の解法は「どういう傾向の組み合わせが最大の価値につながるかが未知」という前提で「全ての組み合わせをしらみつぶしに試す」というアプローチです。
Aの初期値70についてやってみるとポイントが10の場合はベストな組み合わせは2通り、11になると27通りになりました。
投稿2018/11/08 02:24
編集2018/11/08 12:51総合スコア18394
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/11/08 07:53
2018/11/08 12:35
2018/11/08 17:07 編集
2018/11/08 17:21
2018/11/09 02:59
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。