ごりちゃんがゆく

競プロをするゴリラの精進記録

グラフの各連結成分が二部グラフかどうか解析するモジュールを書いた

ごり〜

こんばんは
唐突ですが、当ブログで一番アクセス数が多い記事は6年半前に書いたこちらなんですよね

prd-xxx.hateblo.jp

今でも「二部グラフ 判定」などとググっていただけるとかなり上位にヒットして、ありがたいです

二部グラフの判定が必要になることはまあまあレアなので、自分でも特にスニペット化しておらず、このコードが必要になるたびに自分でもググったりしていました

で、最近 AGC011-C (リンク先ネタバレ注意)を解くことがあって、二部グラフの判定が必要になりました。
でも、自分が昔書いたこのコードは、グラフ全体が二部グラフでないとわかったら即座にFalse を返すので、各連結成分に対して二部グラフ性を調べたいと思ったら、ちょっと書き換えないといけませんでした

なら、各連結成分が二部グラフかどうかを調べるモジュールを書けばいいじゃんと思ったので、書きました
内容は特に難しくないので都度書けば良いレベルでもあるのですが、まあいざ必要になったときに1から実装するのとすでに書いてあるのではコンテストの結果は大きく変わると思うので、将来役に立つことを期待して書きました

これを公開することはライバルの成績を上げかねない (本当か?) のですが、この記事を読んでくださった方は特別です

今回ご紹介するのは

こちらです、 Bipartite
詳しくは上記リンク先の READMEに書かれている通りなのですが、
単純無向グラフに対して、各連結成分が二部グラフかどうかを調べるモジュールです。
中身のロジックはシンプルで、全部の頂点についてdfsをします (ただし、調査済みの頂点ならばスキップします)
dfsで調べている連結成分が二部グラフと仮定して矛盾があれば二部グラフでない、矛盾がなければ二部グラフと判定します。
O(V+E) ぐらいで解析します。

使い方実例、verify

2024/04/04 大幅な速度改善をしました

AGC011-C - Squared Graph

Submission #52010921 - AtCoder Grand Contest 011

bpt = Bipartite(es)
bpt.analize()

a = len(bpt.isolated_vertiecs)
b = len(bpt.bipartite_ccs)
c = len(bpt.not_bipartite_ccs)

ans = 2*N*a - a**2
ans += 2*b*b
ans += 2*b*c
ans += c*c
print(ans)

CODE FESTIVAL 2017 qual B C - 3 Steps

Submission #52010956 - CODE FESTIVAL 2017 qual B

bpt = Bipartite(es)
bpt.analize()

if bpt.is_bipartite():
    b,w = bpt.bipartite_distribution(0)
    print(b*w - M)
else:
    print(N*(N-1)//2 - M)

第10回 アルゴリズム実技検定 過去問 O - 3-順列

Submission #52007642 - 第10回 アルゴリズム実技検定 過去問

bpt = Bipartite(es)
bpt.analize()

B = 1000
dp = set([0])
for x in range(len(bpt.bipartite_ccs)):
    b,w = bpt.bipartite_distribution(x)
    ndp = set()
    for v in dp:
        pb,pw = v//B,v%B
        if pb+b <= c1 and pw+w <= c2:
            ndp.add((pb+b)*B + pw+w)
        if pb+w <= c1 and pw+b <= c2:
            ndp.add((pb+w)*B + pw+b)
    dp |= ndp

iso = len(bpt.isolated_vertiecs)
for v in dp:
    pb,pw = v//B,v%B
    if (c1-pb) + (c2-pw) <= iso:
        exit(print('Yes'))

print('No')

ARC173-B Make Many Triangles の解法の思いつき方

コンテスト中解けるべきだったのに解けなかったので自戒を込めて記事にします

atcoder.jp

問題概要

二次元平面上に相異なるN個の点があるので、各点を1回まで使って、三角形をできるだけ多く作ってください。何個作れますか?
N \le 300 ぐらいです

こう考えればよい!

まず、すべてが一直線上にある場合、三角形は作れないので、答えは0です

つぎに、ほとんどが一直線上にあるけど1個だけその直線上にない場合、1個だけ作れます

2個の場合は2個作れます

3個の場合は3個作れます

こうしていって、 何個まで作れるんだっけ?と考えていくことにしたいが、もともとどんなケースでも  \lfloor \frac{N}{3} \rfloor 個より多く作ることはできません (上界)

実は、通る点が一番多い直線を考えて、その直線を通らない点の個数をxとすると、答えは  \min(x, \lfloor \frac{N}{3} \rfloor ) です!
これだけ!!!!!
最悪ケース (一番なにも作れないケース) から順に考えていくと、自然に解くことができました!!

ちなみに、「通る点が一番多い直線」だけを考える、で本当に良いの?2番目や3番目を意識しなくていいの?
と思うかもしれません (自分も、それが頭をよぎって、こんな単純な方針が見えなかった)

(点がランダムに散らばってる場合など) たいていの場合答えは  \lfloor \frac{N}{3} \rfloor になるので、答えが  \lfloor \frac{N}{3} \rfloorにならなくなるギリギリを考えてみます
 N=10のとき、  \lfloor \frac{N}{3} \rfloor = 3 です
つまり、通常3個作ることができます
このとき、7個までは一直線上にあって大丈夫です

8個めが一直線上にある場合にはじめて、答えが減少します

答えに影響するのは、  N - \lfloor \frac{N}{3} \rfloor より多い点が一直線上にあるときに限るとわかります
 N - \lfloor \frac{N}{3} \rfloor より多い点が一直線上にあるとき、別の直線上に  N - \lfloor \frac{N}{3} \rfloor より多い点があることはどう頑張ってもありませんよね。

これが2番目3番目...に通る点が多い直線を考えなくて良い理由です!

教訓

最悪ケースを考えるべし!!!!

ACコード (コンテスト後)

lines[(i,j)] に、点iと点jを通る直線に何個の点が含まれますか?を管理しています
is_line() は3点が同一直線上にあるかの判定 (傾きの比較を変形して割り算を回避してます) このスニペット持ってると役立ちそう

N = int(input())
XY = [tuple(map(int,input().split())) for _ in range(N)]

def is_line(x1,y1,x2,y2,x3,y3):
    return (x2-x1)*(y3-y1) == (y2-y1)*(x3-x1)

lines = {(0,1): 2}
for k in range(2,N):
    xk,yk = XY[k]
    add = []
    for (i,j) in lines.keys():
        xi,yi = XY[i]
        xj,yj = XY[j]
        if is_line(xi,yi,xj,yj,xk,yk):
            lines[(i,j)] += 1
        else:
            add.append((i,k))
            add.append((j,k))
    for i,j in add:
        lines[(i,j)] = 2

line_max = max(lines.values())
other = N - line_max
ans = min(N//3, other)
print(ans)

(Python/PyPy) LowLinkを非再帰で実装しました!

お気持ち

Python で LowLinkを写経してACしようとしたらTLEしちゃったんですがググって出てきたやつは大体再帰で書かれていて、非再帰のLowLinkはざっとみた感じは見つからなかったので、非再帰で書いてみました!
再帰なので、PyPyで高速に動作すると思います!

参考

理解や実装にあたり、Senさんの以下の記事を大変参考にさせていただきました!
sen-comp.hatenablog.com

リンク先の

も参考になりました!ありがとうございます!

こちらにアップしました!
使い方などは下記の README.md をご参照ください!

github.com

コード

class LowLink:
    def __init__(self,n,es):
        self.n = n
        self.es = es
        self.visited = [0] * n
        self.order = [0] * n
        self.low = [0] * n
        self.count = 0
        self.articulation_points = set()
        self.bridges = []
        self.dfs_parent = [-1] * n
        self.dfs_child = [[] for _ in range(n)]
        self.is_dfs_root = [0] * n
    def _dfs(self,rt):
        self.is_dfs_root[rt] = 1
        stack = [(-1,rt,0)]
        while stack:
            p,c,dir = stack.pop()
            if dir==0:
                if self.visited[c]: continue
                self.visited[c] = 1
                self.low[c] = self.order[c] = self.count
                self.count += 1
                if c != rt:
                    self.dfs_parent[c] = p
                    self.dfs_child[p].append(c)
                for to in self.es[c][::-1]:
                    if self.visited[to]:
                        if to != p:
                            self.low[c] = min(self.low[c], self.order[to])
                        continue
                    stack.append((c,to,1))
                    stack.append((c,to,0))
            else:
                if self.dfs_parent[c] != p: continue
                if c != self.dfs_parent[p]:
                    self.low[p] = min(self.low[p], self.low[c])
                if p != rt and self.order[p] <= self.low[c]:
                    self.articulation_points.add(p)
                if self.order[p] < self.low[c]:
                    self.bridges.append((p,c)) 
        if len(self.dfs_child[rt]) >= 2:
            self.articulation_points.add(rt)
    def build(self):
        self.component_num = 0
        for i in range(self.n):
            if self.visited[i]: continue
            self._dfs(i)
            self.component_num += 1

verify

関節点

AOJ GRL_3_A

AOJ GRL_3_B

order, low などなど

ABC334-G

おわり

いつか誰かの役に立ったらうれしい!!!

ABC334-B Christmas Trees を場合分けもできるだけ数学的な頭も使わない解き方を考えてみる

1日に2本も記事を書くなんて珍しいな! クリスマスイブだしな!
ってなわけで書いてみる!

昨日の ABC334-B ですが、ABCのB問題にしては「数学」をしましょうって感じでつらかったと感じた人も多かったのではないでしょうか
ゴリもそこそこ混乱しました

そこで、

  • 場合分けをしない
  • 数学的な頭をできるだけ使わない

解き方を考えてみました
「できるだけ」なので、これでも難しい可能性もありますし、もっと良い方法もあるかもしれません。
図をかいてみたので数学の頭でなく感覚でつかんでもらえるとうれしいです!

問題概要

数直線があります。座標Aを起点に、Mおきにクリスマスツリーが置かれます。座標L,R の間 (両端も含む、L \le R) に、クリスマスツリーは何本ありますか?
A,M,L,R は クソデカなのでループを使ってはいけません。
ABC334-B リンク

ゴリの考えたさいきょうの解き方

3ステップあります

1. Ax=0 の位置に平行移動させる

公式解法の考え方も初手これでした。
(ゴリはコンテスト中は L0 のところに移動してしまったためちょっと大変でした)

平行移動

これをすると、クリスマスツリーの位置が M の倍数のところになるので、かなり見通しがよくなります。 (本番で思いつきたかった...)
なお、必ずしもこの図の通りではありません (LAの右側にあったり、RAの左側にあったりします) が、いずれであっても以降の考え方を適用できます

2. L',R' を、M で割る!

Mで割ります!
これをすることによって、世界全体が  \frac{1}{M} になります!
下の図のように、0を中心に、世界がぐにゃっと縮みます!

Mで割る

これによって、座標Mごとに生えていたクリスマスツリーは、座標1ごとに生えていることになり、
つまり 整数の座標に 生えていることになります!
ただし、注意点として、L''R''\frac{L’}{M}\frac{R’}{M} といった 有理数となります (整数とは限らない)
(さらに実装上の注意なのですが、PythonL/M のように演算すると float型となり、精度が足りなくてWAとなります、回避する方法を後述します)

3. L''を切り上げ、R'' を切り捨てし、整数にする

L''R''が中途半端な位置にあるので、それぞれ切り上げ・切り捨てし、整数にしましょう!
これによりL'''R'''の間にいくつ整数があるか、計算できます!

整数にする

これでほぼ答えは出ているのですが、最後に注意としては
クリスマスツリーはL'''R'''の両端を数えるので、R-L で求まる距離に 1 を足しましょう!
(上の図だと、R'''-L''' は4だけど、整数は端点を含むと5つある)

ACコード (その1)

A,M,L,R = map(int,input().split())
L -= A
R -= A
from decimal import Decimal
L = Decimal(L) / Decimal(M)
R = Decimal(R) / Decimal(M)
from math import ceil,floor
L = ceil(L)
R = floor(R)
print(R - L + 1)
  1. 2~3行目で LRを平行移動しています
  2. 4~6行目で LRをそれぞれMで割っています
    (先述の通り、単に L/M などとすると精度がたりないので、Decimal で精度の良い計算をしています)
  3. 7~9行目で切り上げ、切り捨てをしています

submission

ACコード (その2)

A,M,L,R = map(int,input().split())
L -= A
R -= A
from fractions import Fraction
L = Fraction(L, M)
R = Fraction(R, M)
from math import ceil,floor
L = ceil(L)
R = floor(R)
print(R - L + 1)

その1とほぼ同じですが、2. のところは Fraction でもできます!

submission

おわり

もっとシンプルな解法などあれば教えてくださるとうれしいです!

オンサイトやオフ会に役立つおようふく

ゴリ~~
(こんばんは!メリークリスマス!)

おようふくアドベントカレンダー2023 の執筆を後回しにしていたら、クリスマスイブになっていました!
というわけで 12/24の記事です!
執筆するぞ!
執筆というよりたぶん画像ペタペタするだけ!

競プロerであることを示す系

初めてのオフ会やオンサイトなど、待ち合わせがうまくいかなかったらどうしよう、とか、
会話の輪に入れなかったらどうしよう、とか、
いろいろな不安があると思います
が、「自分は競プロerだぞ」なおようふくを着ていることで、他の競プロerに認知してもらえたり、
会話のきっかけになったりで、アドを得ることができます!

入手しやすいやつ (おすすめ)

AtCoder公式のSUZURIです!

suzuri.jp

AtCoderパーカー

たぶんもう3年ぐらい着てます!
AtCoderのロゴがドーンと主張していて、もしかして街中でAtCoderユーザーとすれ違ったら 「AtCoderだ!!」って思われたかもしれません
オフ会など 競プロerとお会いするとき、いいですね!とよく言ってもらえます
ロゴ入りの青いTシャツも持っていました(青コーダーだぞ!と誇るために) が、着ふるしすぎてロゴの印刷が消えました 寿命です
そろそろ黄色を買う時期でしょうか...
買いましょう!

競プロするねこのSUZURIです!

suzuri.jp

こちら、競プロするねこのSUZURIなんですが、品ぞろえが豊富なのと、とにかくこの白いねこちゃんの絵がかわいいんですよ!

バグを直すとスコアが下がる

これやばくないですか!?!?!? これかわいすぎて持ってるだけでムヒヒヒヒってなってしまうし、着たらさらにムヒヒヒヒってなってしまいます (個人の感想です)
「バグを直すとスコアが下がる」は、AHCなどでまれによくある謎事象なのですが、これは会話もはずむのでおすすめです
やば!かわいい!って思った人は買いましょう!

なお、SUZURIは全般的に品質が良く、かなり実用的です!
ヘビロテ用の普段着としても大変役に立ちます!
上記2リンクは AtCoder Clans のトップページからも行くことができますので覚えていてください!
たまにセール等もやっているようなのでお見逃しなく!

入手困難系 (持ってるとつよい)

入手困難というか、いわゆる非売品の「競プロ衣類」について話します
決勝ありのコンテストで決勝に進んだり、所定の成績を収めたり、最近はたま~~に「飛び賞」などで得ることもある、あれです!
ごく一部の人しか持っていないレアリティもあるし、これを着ていると競プロ強者だなという目で見られること間違いなしです!
ゴリはTシャツを 3枚だけ持っています!

2017年 DDCC

なんか奇跡がおこって本戦に招待された回です →参加記
めちゃめちゃ刺激を受けたな~~ 人生で一番かもしれない

2018年 MUJIN Programming Challenge 2018

100位までTシャツ、200位までオフィスツアーだったんですが、オフィスツアーに運良く行ったと思ったら「ここに来た人にもTシャツ送るよ」と言ってくれてMUJINさん神でした
また開いてくれないかな...

2021年 日本橋ハーフマラソン

なんか大当たりして爆勝ちしちゃった回です →参加記

最後にもらえたの2021年だ!最近もらえてない!欲しい!!!

思えば、ゴリはずっと Google Code Jamの Tシャツにあこがれていましたね
もう GCJは終わってしまったので叶わないんですが...
最近だと yuusanlondonさんが GCJ Tシャツを着てて、やっぱかっけええ~って思いました

次Tシャツのねらい目かなと思っているのは Meta Hacker Cupで、これはまだ毎年やっているのですが、時間が深夜すぎて (AM2~5時とか) で、昨年今年とギブアップしています
体力をつけて来年は挑戦したい...

あとなんか Petrさんは強すぎて競プロ衣類を集めすぎてしまったので奥さんが縫い合わせて布団カバーにしたという逸話もあったりしますね なんじゃそりゃ贅沢すぎる

自分のアイデンティティを示す系

これ!名札替わりにもなるのでおすすめです!
Xなどでキャラの存在感をある程度もっている必要がありますが、たとえば自分の場合はゴリラ柄の服を着ていると、「ごりちゃんですね!」とか「いつもツイッターで見てます!」とかよく話しかけてもらえます!

ゴリラ柄 パーカー

2019年から着てるんですね~(新品のときはこんなに印刷がくっきりしていたのか)
これ、普段着としてもかなり頻繁に着ているので、ゴリとたくさんあったことのある人は何度も見ているかと思います
かわいくて気に入ってるので 2着目が超ほしい!!

ゴリラ柄シャツ

これは今年買いました!
パーカーだけだと夏に困ると思ったので!
実際このシャツも何度かオンサイトで着ましたね!

GORILLA BEER

飲み会用です!パリピごりちゃん!ウェイ!!!ww

いかがでしたか!

おようふくで差をつけろ!
よいクリスマスを!よいお年を!

ABC330-E Mex and Update を無思考で解けるライブラリを書いた

ごり〜

こんばんは
先日のABC330-E を無思考で解けるライブラリを書いてみたので、紹介させていただきます!

問題概要は、 配列Aと、クエリとして ix が与えられるので、
クエリのたびに A[i]x に変更し、配列AのMEXを出力してください
といったものです。
MEXとは、配列に含まれない最小の非負整数をさします。

早速ですが、使い方はこんな感じになります

mex = Mex(A)
for i,x in qs:
    i -= 1
    mex.remove(A[i])
    mex.add(x)
    A[i] = x
    print(mex.get())

シンプルですね!
最近 MEXという概念がよく出るので、書いてみました!

ちなみに書いてみた動機は、AllDirectionsさんのポストを見ていいなあ〜と思ったからでした

動作原理とか

(解法のネタバレを含みます)

この問題を解こうとしたときに、どんなデータ構造があれば解けるのかな?と考えるはずです。
集合に含まれる要素の個数は管理できるとしても、含まれない非負整数のうち最小のものはどうやって求めるのか?と考えると思います。
実は、MEXというのは集合の要素数Nであるとき、Nより大きい値は取らないという特徴があります。
となれば、想定される最大のN までの非負整数のうち、集合に含まれないものを全部管理してしまえ!と発想できれば、解けた!に大きく近づきます
ところが、Python で解くには set で集合を管理できはしても高速に最小値を取れない、あるいは、heapqで 最小値を高速に取得できたとしても変更に弱いという難点があり、もう一山あります
(例えば C++ では std::set があります、Pythonでは tatyamさんの SortedSetsortedcontainers を使うのが定跡のようです)
今回、これらを使わずとも、(機能を落としているぶん) 早く動作するモジュールを書きました!
原理としては、内部に heapq を2本持ち、本来 heapqでは削除できないところ、擬似的な削除を実現しています。

コードは下記で公開しています!
ぜひ使ってみてください!
github.com

from heapq import heappop,heappush,heapify
class Mex:    
    def __init__(self, arr=[], MAX=10**6) -> None:
        self.MAX = MAX + 5
        self.hist = [0] * (self.MAX+1)
        for a in arr:
            if a > self.MAX: continue
            self.hist[a] += 1
        self.q = []
        self.d = []
        for i in range(self.MAX+1):
            if self.hist[i] == 0:
                heappush(self.q, i)
    def get(self):
        while self.q and self.d and self.q[0]==self.d[0]:
            heappop(self.q)
            heappop(self.d)
        return self.q[0] if self.q else None
    def add(self, x):
        if x > self.MAX: return
        self.hist[x] += 1
        if self.hist[x] == 1:
            heappush(self.d, x)
    def remove(self, x):
        if x > self.MAX: return
        self.hist[x] -= 1
        if self.hist[x] == 0:
            heappush(self.q, x)

集合に含まれないものを管理する、というのは、MEXならではのある種の典型の気もします
が、そんなことも考えず、ライブラリ化されてると嬉しいよね!という気持ちです
verify提出

告知

ところで、告知 (というほど大げさなものではない) ですが、
ごりちゃん競プロライブラリ」を始動します(した)
今後、ごりちゃんが便利と思ったコードはこちらにあげていく (つもり) (予定) なので、要チェックしてみてください!!
では! 良き競プロライフを!

QuizKnockの鶴崎さんが解いていたパズルのプログラムを自分も書いてみた

QuizKnockのこちらの動画が競プロerの間で話題になっていましたね!
まだ見てない方はご覧ください!

鶴崎さんはクイズ王しながら黄コーダーだったんですよね... やばすぎ
憧れます

パズルの概要はこんな感じです!

  • 3,4,7,8 を使って四則演算で 10を作れみたいなやつ
  • 3,4,7,8 の部分は4個だったり5個だったり6個だったり可変で与えられる
  • 10の部分も入力で与えられる
  • 自由に並び替えて良い、括弧を使って良い
  • 34みたいに桁をくっつけちゃダメ
  • 階乗とかもだめ (四則演算じゃないからね)
  • 単項マイナス (3を-3にするとか) も多分だめ (言及されてないけど)

難しいところ

動画内でも説明されていますが、、、
まず素朴な解法を考えると、順列全探索という方法が浮かびます
並び替えた順列を全て試し、左から全ての +-*/ の組み合わせを適用して試していく、というものです
一見良さそうに見えるのですが、この方法ではだめなケースがあり、
3*4+5*6 のように、トーナメントのような順序で計算されるパターンがカバーできません
そこで、「式」という概念を以下のように考えました
「式」は、以下のどちらかからなる

  • 単項 (1 など)
  • 左の式、演算子、右の式 の組み合わせ (式 + 式 など)

これを使うと、括弧を自由に使って計算順序を指定したときの二分木の構造が表せます!
(後で教えていただいたんですが、この部分は逆ポーランド記法 を使うとスタックで実装できて楽だったかもしれません)

また、途中の計算結果は、bitDPみたいな持ち方をしました
例えば、コード中の results[13][5] の部分には、A[0], A[2], A[3] を全て使って 5 という結果になる式があれば、それが入ります
(※13 は 2進数では 1101 で、0番目と2番目と3番目のbitが立っているため)

コード

コード公開処刑の時間です
見たい方はどうぞ
GitHubはこちら

クリックすると展開されます

class Formula:
    def __init__(self, used_bit, x, left_formula=None, right_formula=None, operator=None):
        self.used_bit = used_bit
        self.x = x
        self.left_formula = left_formula
        self.right_formula = right_formula
        self.operator = operator
        self.has_multi_terms = operator is not None and operator in '+-'
    def add(self, right_formula):
        assert (self.used_bit & right_formula.used_bit) == 0
        return Formula(self.used_bit|right_formula.used_bit, self.x + right_formula.x, self, right_formula, '+')
    def sub(self, right_formula):
        assert (self.used_bit & right_formula.used_bit) == 0
        return Formula(self.used_bit|right_formula.used_bit, self.x - right_formula.x, self, right_formula, '-')
    def prod(self, right_formula):
        assert (self.used_bit & right_formula.used_bit) == 0
        return Formula(self.used_bit|right_formula.used_bit, self.x * right_formula.x, self, right_formula, '*')
    def div(self, right_formula):
        assert (self.used_bit & right_formula.used_bit) == 0
        assert right_formula.x != 0
        return Formula(self.used_bit|right_formula.used_bit, self.x / right_formula.x, self, right_formula, '/')
    def __str__(self):
        if self.left_formula is None:
            return str(self.x)
        # 余計な括弧はつけないぞという謎のこだわりポイント
        _left = str(self.left_formula)
        left = '({})'.format(_left) if self.left_formula.has_multi_terms and self.operator in '*/' else _left
        _right = str(self.right_formula)
        if self.right_formula.has_multi_terms:
            right = '({})'.format(_right) if self.right_formula.has_multi_terms and self.operator != '+' else _right
        else:
            right = '({})'.format(_right) if self.right_formula.has_multi_terms and self.operator == '/' \
                and self.right_formula.left_formula is not None \
                or self.right_formula.operator == '/' else _right
        return '{}{}{}'.format(left, self.operator, right)

from fractions import Fraction
class MakeXSolver:
    def __init__(self, A, X):
        assert len(A) <= 10, 'too many elements to solve: {}'.format(len(A))
        self.A = A
        self.X = X
        self.N = len(A)
    def solve(self):
        # results[s][x] には、集合sに含まれる要素i番目のA[i]をすべて使ってxを作る作り方を高々1つ格納していく
        results = [{} for _ in range(1<<self.N)]
        for i,a in enumerate(self.A):
            x = Fraction(a)
            results[1<<i][x] = Formula(1<<i, x)
        for s in range(1,1<<self.N):
            if bin(s).count('1') <= 1: continue
            #sの部分集合tを列挙し、u=s\t との組み合わせで作れる結果をすべて results[s]に入れる
            t = s
            while t:
                t = (t-1)&s
                u = s^t
                assert (t&u)==0
                for x,l in results[t].items():
                    for y,r in results[u].items():
                        if x+y not in results[s] and t < u:
                            results[s][x+y] = l.add(r)
                        if x-y not in results[s]:
                            results[s][x-y] = l.sub(r)
                        if x*y not in results[s] and t < u:
                            results[s][x*y] = l.prod(r)
                        if y != 0 and x/y not in results[s]:
                            results[s][x/y] = l.div(r)
        if self.X in results[-1]:
            print('{}={}'.format(results[-1][X], X))
        else:
            print('{} is not found'.format(X))
            print('found results are below')
            for x,f in sorted(results[-1].items()):
                if x.denominator==1:
                    print('{}={}'.format(f, x))
                else:
                    print('{}={}/{}'.format(f, x.numerator, x.denominator))

*A,X = list(map(int,input().split()))
solver = MakeXSolver(A,X)
solver.solve()

実行結果の例

実行結果
左結合だけじゃなかったり括弧を使ったりしてますが、ちゃんと解を見つけられていそうです

余計な括弧を使わないという謎のこだわりポイント

最初、例えば 3*(4/3+7)=25 という式は (3*((4/3)+7))=25 のように出力していました
が、前者のほうがすっきりしていいですよね!? 前者を出力する改善を地味にやってました
結果、メンテする気の起こらないやばコードが生成されました...
なんだかんだ100個ぐらいは結果を流し見したので大丈夫だと信じたい!
(TODO) ちゃんと頭が回るときに検証したり説明を書いたりするつもり
テストコードないのやばいね

計算量

激ヤバだと思います 集合の部分集合列挙するところ、内側が定数なら  O(3^N) らしいですが、そこの内側がやばいですね
部分集合t を全部使ったときにあり得る結果全て * 部分集合u を全部使ったときにあり得る結果全て * 4種類の演算 を全部試しています
(TODO) 計算量あとでちゃんと考える
5個までは一瞬、6個だと数秒かかります
7個は実行するのが怖い...

おわり

記事書きかけみたいな感じで公開しちゃってすみません
どちらかというといろんな人の実装を見たさがあります...!
皆さんもやってみてください!
あと、ここ改善できそう... みたいなご意見もいただけるとうれしいです!