[{"data":1,"prerenderedAt":2044},["ShallowReactive",2],{"content-query-8ioi2Dph7w":3,"link-card-/articles/tech/development/yomogi-constraint-driven-design":2041},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"tags":11,"rowTypeId":17,"sitemap":18,"body":19,"_type":2035,"_id":2036,"_source":2037,"_file":2038,"_stem":2039,"_extension":2040},"/articles/tech/development/diff-lcs-two-level","development",false,"","大学で習った動的計画法で差分比較ツールを実装する｜LCS + DP の行・文字二段差分","大学のアルゴリズム講義で習った動的計画法（DP）を実用ツールに落とし込みたくて、ブラウザ完結の差分比較ツールを LCS + DP で実装した記録です。どのように動作しているか設計をまとめます。","2026-04-29",[12,13,14,15,16],"差分比較ツール","LCS","アルゴリズム","TypeScript","動的計画法",1,{"loc":4,"lastmod":10,"priority":17},{"type":20,"children":21,"toc":2020},"root",[22,30,36,49,67,72,151,157,163,168,175,180,195,200,205,210,223,228,246,251,256,269,274,279,297,310,316,321,970,982,988,994,1006,1012,1033,1324,1345,1351,1399,1903,1938,1950,1955,1960,1965,1970,2009,2014],{"type":23,"tag":24,"props":25,"children":27},"element","h2",{"id":26},"はじめに",[28],{"type":29,"value":26},"text",{"type":23,"tag":31,"props":32,"children":33},"p",{},[34],{"type":29,"value":35},"大学の講義で動的計画法（DP）を習ったとき、せっかくなら習いっぱなしにせず実用的なツールに落とし込みたいと思いました。題材を探していて思い当たったのが、自分自身がコードレビューや原稿校正で日常的に使う「差分比較」です。",{"type":23,"tag":31,"props":37,"children":38},{},[39,41,47],{"type":29,"value":40},"この記事では、自作の ",{"type":23,"tag":42,"props":43,"children":45},"a",{"href":44},"/tools/diff",[46],{"type":29,"value":12},{"type":29,"value":48}," を LCS + DP で実装するにあたってやったことや行レベル・文字レベルの二段差分という設計をまとめます。",{"type":23,"tag":50,"props":51,"children":52},"summary-box",{},[53],{"type":23,"tag":31,"props":54,"children":55},{},[56,58,65],{"type":29,"value":57},"ブラウザ完結の差分比較ツールを、大学で習った動的計画法（DP）で実装した記録です。LCS（最長共通部分列）は DP の典型問題で、自分の手で書ける手触りのちょうど良さがありました。行レベルでまず差分を取り、",{"type":23,"tag":59,"props":60,"children":62},"code",{"className":61},[],[63],{"type":29,"value":64},"changed",{"type":29,"value":66}," ペアに対して文字レベルで再差分する二段構えで、実用度も担保しています。",{"type":23,"tag":24,"props":68,"children":70},{"id":69},"環境",[71],{"type":29,"value":69},{"type":23,"tag":73,"props":74,"children":75},"table",{},[76,95],{"type":23,"tag":77,"props":78,"children":79},"thead",{},[80],{"type":23,"tag":81,"props":82,"children":83},"tr",{},[84,90],{"type":23,"tag":85,"props":86,"children":87},"th",{},[88],{"type":29,"value":89},"項目",{"type":23,"tag":85,"props":91,"children":92},{},[93],{"type":29,"value":94},"技術",{"type":23,"tag":96,"props":97,"children":98},"tbody",{},[99,113,125,138],{"type":23,"tag":81,"props":100,"children":101},{},[102,108],{"type":23,"tag":103,"props":104,"children":105},"td",{},[106],{"type":29,"value":107},"フレームワーク",{"type":23,"tag":103,"props":109,"children":110},{},[111],{"type":29,"value":112},"Nuxt 4 + Vue 3",{"type":23,"tag":81,"props":114,"children":115},{},[116,121],{"type":23,"tag":103,"props":117,"children":118},{},[119],{"type":29,"value":120},"言語",{"type":23,"tag":103,"props":122,"children":123},{},[124],{"type":29,"value":15},{"type":23,"tag":81,"props":126,"children":127},{},[128,133],{"type":23,"tag":103,"props":129,"children":130},{},[131],{"type":29,"value":132},"差分計算",{"type":23,"tag":103,"props":134,"children":135},{},[136],{"type":29,"value":137},"ブラウザ内（サーバー送信なし）",{"type":23,"tag":81,"props":139,"children":140},{},[141,146],{"type":23,"tag":103,"props":142,"children":143},{},[144],{"type":29,"value":145},"対象テキスト長",{"type":23,"tag":103,"props":147,"children":148},{},[149],{"type":29,"value":150},"数千行程度まで想定",{"type":23,"tag":24,"props":152,"children":154},{"id":153},"なぜlcs-dpを利用する判断をしたか",[155],{"type":29,"value":156},"なぜLCS + DPを利用する判断をしたか",{"type":23,"tag":158,"props":159,"children":161},"h3",{"id":160},"調査パート",[162],{"type":29,"value":160},{"type":23,"tag":31,"props":164,"children":165},{},[166],{"type":29,"value":167},"大学で習った内容は、音声認識に関するDPだったので、文字列を対象にしていませんでした。なので、まずどのようなアプローチで文字列比較をDPで行うかという部分を調べる必要がありました。調べたところ、LCSという最長共通部分列を求める問題を解けば良いことがわかりました。",{"type":23,"tag":169,"props":170,"children":172},"h4",{"id":171},"lcs-の基本1行同士で考える",[173],{"type":29,"value":174},"LCS の基本：1行同士で考える",{"type":23,"tag":31,"props":176,"children":177},{},[178],{"type":29,"value":179},"最長共通部分列の具体例を挙げます。まず２つの文字列があるとします。変更前、変更後の順です。",{"type":23,"tag":181,"props":182,"children":183},"ul",{},[184,190],{"type":23,"tag":185,"props":186,"children":187},"li",{},[188],{"type":29,"value":189},"あああああ",{"type":23,"tag":185,"props":191,"children":192},{},[193],{"type":29,"value":194},"いあああい",{"type":23,"tag":31,"props":196,"children":197},{},[198],{"type":29,"value":199},"２つの文字列から最長共通部分列を求めると、「あああ」が該当します。",{"type":23,"tag":169,"props":201,"children":203},{"id":202},"複数行への応用",[204],{"type":29,"value":202},{"type":23,"tag":31,"props":206,"children":207},{},[208],{"type":29,"value":209},"さらに実用的な例として、複数行の文字列を考えてみます。",{"type":23,"tag":181,"props":211,"children":212},{},[213,218],{"type":23,"tag":185,"props":214,"children":215},{},[216],{"type":29,"value":217},"あ\\nああ\\nああ",{"type":23,"tag":185,"props":219,"children":220},{},[221],{"type":29,"value":222},"い\\nあああい",{"type":23,"tag":31,"props":224,"children":225},{},[226],{"type":29,"value":227},"差分比較では行ごとに対応付けて比較します。対応する行同士で最長共通部分列を求めると、以下のような結果になります。",{"type":23,"tag":181,"props":229,"children":230},{},[231,236,241],{"type":23,"tag":185,"props":232,"children":233},{},[234],{"type":29,"value":235},"1行目（あ vs い）→ 共通文字なし",{"type":23,"tag":185,"props":237,"children":238},{},[239],{"type":29,"value":240},"2行目（ああ vs あああい）→ 「ああ」が共通",{"type":23,"tag":185,"props":242,"children":243},{},[244],{"type":29,"value":245},"3行目（ああ vs 対応行なし）→ 文字列2に該当行なし",{"type":23,"tag":31,"props":247,"children":248},{},[249],{"type":29,"value":250},"これでわかることは、1行目は完全に変更されているということ、2行目は「ああ」が変更されていないということ、3行目は削除されたということです。これが文字列差分比較の大まかな仕組みです。",{"type":23,"tag":169,"props":252,"children":254},{"id":253},"他の差分アルゴリズム",[255],{"type":29,"value":253},{"type":23,"tag":31,"props":257,"children":258},{},[259,261,267],{"type":29,"value":260},"差分計算アルゴリズムには他のものもあり、Myers アルゴリズム（",{"type":23,"tag":59,"props":262,"children":264},{"className":263},[],[265],{"type":29,"value":266},"git diff",{"type":29,"value":268}," 等で使われる O(ND) 法）など、より洗練された手法が存在するようです。実用面ではそちらの方が強力ですし、世の中の差分ライブラリも豊富です。",{"type":23,"tag":158,"props":270,"children":272},{"id":271},"今回のユースケースに照らし合わせる",[273],{"type":29,"value":271},{"type":23,"tag":31,"props":275,"children":276},{},[277],{"type":29,"value":278},"今回のツールが対象とするのは次のような状況に限られます。",{"type":23,"tag":181,"props":280,"children":281},{},[282,287,292],{"type":23,"tag":185,"props":283,"children":284},{},[285],{"type":29,"value":286},"プログラマーがコードレビュー前に数十行〜数百行を見比べたい",{"type":23,"tag":185,"props":288,"children":289},{},[290],{"type":29,"value":291},"編集者が原稿の校正で段落単位の変更を確認したい",{"type":23,"tag":185,"props":293,"children":294},{},[295],{"type":29,"value":296},"契約書担当者が条文の版ごとの違いを見たい",{"type":23,"tag":31,"props":298,"children":299},{},[300,302,308],{"type":29,"value":301},"どれも ",{"type":23,"tag":303,"props":304,"children":305},"strong",{},[306],{"type":29,"value":307},"1画面におさまる長さ",{"type":29,"value":309}," が大半です。この規模なら O(m·n) の計算量でも体感的に一瞬で終わるため、単純なLCS＋DPの実装で問題ないと判断しました。",{"type":23,"tag":24,"props":311,"children":313},{"id":312},"基本lcs-テーブルと-dp",[314],{"type":29,"value":315},"基本：LCS テーブルと DP",{"type":23,"tag":31,"props":317,"children":318},{},[319],{"type":29,"value":320},"LCS (Longest Common Subsequence) は「2つの列の最長共通部分列」を求める典型的な DP 問題です。",{"type":23,"tag":322,"props":323,"children":327},"pre",{"className":324,"code":325,"language":326,"meta":7,"style":7},"language-typescript shiki shiki-themes vitesse-dark","const lcsTable: number[][] = Array(len1 + 1)\n  .fill(null)\n  .map(() => Array(len2 + 1).fill(0))\n\nfor (let i = 1; i \u003C= len1; i++) {\n  for (let j = 1; j \u003C= len2; j++) {\n    if (str1[i - 1] === str2[j - 1]) {\n      lcsTable[i][j] = lcsTable[i - 1][j - 1] + 1\n    } else {\n      lcsTable[i][j] = Math.max(lcsTable[i - 1][j], lcsTable[i][j - 1])\n    }\n  }\n}\n","typescript",[328],{"type":23,"tag":59,"props":329,"children":330},{"__ignoreMap":7},[331,397,424,493,503,580,648,724,808,826,943,952,961],{"type":23,"tag":332,"props":333,"children":335},"span",{"class":334,"line":17},"line",[336,342,348,354,360,365,371,376,381,386,392],{"type":23,"tag":332,"props":337,"children":339},{"style":338},"--shiki-default:#CB7676",[340],{"type":29,"value":341},"const ",{"type":23,"tag":332,"props":343,"children":345},{"style":344},"--shiki-default:#BD976A",[346],{"type":29,"value":347},"lcsTable",{"type":23,"tag":332,"props":349,"children":351},{"style":350},"--shiki-default:#666666",[352],{"type":29,"value":353},": ",{"type":23,"tag":332,"props":355,"children":357},{"style":356},"--shiki-default:#5DA994",[358],{"type":29,"value":359},"number",{"type":23,"tag":332,"props":361,"children":362},{"style":350},[363],{"type":29,"value":364},"[][] =",{"type":23,"tag":332,"props":366,"children":368},{"style":367},"--shiki-default:#80A665",[369],{"type":29,"value":370}," Array",{"type":23,"tag":332,"props":372,"children":373},{"style":350},[374],{"type":29,"value":375},"(",{"type":23,"tag":332,"props":377,"children":378},{"style":344},[379],{"type":29,"value":380},"len1",{"type":23,"tag":332,"props":382,"children":383},{"style":338},[384],{"type":29,"value":385}," + ",{"type":23,"tag":332,"props":387,"children":389},{"style":388},"--shiki-default:#4C9A91",[390],{"type":29,"value":391},"1",{"type":23,"tag":332,"props":393,"children":394},{"style":350},[395],{"type":29,"value":396},")\n",{"type":23,"tag":332,"props":398,"children":400},{"class":334,"line":399},2,[401,406,411,415,420],{"type":23,"tag":332,"props":402,"children":403},{"style":350},[404],{"type":29,"value":405},"  .",{"type":23,"tag":332,"props":407,"children":408},{"style":367},[409],{"type":29,"value":410},"fill",{"type":23,"tag":332,"props":412,"children":413},{"style":350},[414],{"type":29,"value":375},{"type":23,"tag":332,"props":416,"children":417},{"style":338},[418],{"type":29,"value":419},"null",{"type":23,"tag":332,"props":421,"children":422},{"style":350},[423],{"type":29,"value":396},{"type":23,"tag":332,"props":425,"children":427},{"class":334,"line":426},3,[428,432,437,442,447,451,455,460,465,470,475,479,483,488],{"type":23,"tag":332,"props":429,"children":430},{"style":350},[431],{"type":29,"value":405},{"type":23,"tag":332,"props":433,"children":434},{"style":367},[435],{"type":29,"value":436},"map",{"type":23,"tag":332,"props":438,"children":439},{"style":350},[440],{"type":29,"value":441},"(()",{"type":23,"tag":332,"props":443,"children":444},{"style":350},[445],{"type":29,"value":446}," =>",{"type":23,"tag":332,"props":448,"children":449},{"style":367},[450],{"type":29,"value":370},{"type":23,"tag":332,"props":452,"children":453},{"style":350},[454],{"type":29,"value":375},{"type":23,"tag":332,"props":456,"children":457},{"style":344},[458],{"type":29,"value":459},"len2",{"type":23,"tag":332,"props":461,"children":462},{"style":338},[463],{"type":29,"value":464}," +",{"type":23,"tag":332,"props":466,"children":467},{"style":388},[468],{"type":29,"value":469}," 1",{"type":23,"tag":332,"props":471,"children":472},{"style":350},[473],{"type":29,"value":474},").",{"type":23,"tag":332,"props":476,"children":477},{"style":367},[478],{"type":29,"value":410},{"type":23,"tag":332,"props":480,"children":481},{"style":350},[482],{"type":29,"value":375},{"type":23,"tag":332,"props":484,"children":485},{"style":388},[486],{"type":29,"value":487},"0",{"type":23,"tag":332,"props":489,"children":490},{"style":350},[491],{"type":29,"value":492},"))\n",{"type":23,"tag":332,"props":494,"children":496},{"class":334,"line":495},4,[497],{"type":23,"tag":332,"props":498,"children":500},{"emptyLinePlaceholder":499},true,[501],{"type":29,"value":502},"\n",{"type":23,"tag":332,"props":504,"children":506},{"class":334,"line":505},5,[507,513,518,523,528,533,537,542,547,552,557,561,565,570,575],{"type":23,"tag":332,"props":508,"children":510},{"style":509},"--shiki-default:#4D9375",[511],{"type":29,"value":512},"for",{"type":23,"tag":332,"props":514,"children":515},{"style":350},[516],{"type":29,"value":517}," (",{"type":23,"tag":332,"props":519,"children":520},{"style":338},[521],{"type":29,"value":522},"let ",{"type":23,"tag":332,"props":524,"children":525},{"style":344},[526],{"type":29,"value":527},"i",{"type":23,"tag":332,"props":529,"children":530},{"style":350},[531],{"type":29,"value":532}," =",{"type":23,"tag":332,"props":534,"children":535},{"style":388},[536],{"type":29,"value":469},{"type":23,"tag":332,"props":538,"children":539},{"style":350},[540],{"type":29,"value":541},";",{"type":23,"tag":332,"props":543,"children":544},{"style":344},[545],{"type":29,"value":546}," i",{"type":23,"tag":332,"props":548,"children":549},{"style":350},[550],{"type":29,"value":551}," \u003C=",{"type":23,"tag":332,"props":553,"children":554},{"style":344},[555],{"type":29,"value":556}," len1",{"type":23,"tag":332,"props":558,"children":559},{"style":350},[560],{"type":29,"value":541},{"type":23,"tag":332,"props":562,"children":563},{"style":344},[564],{"type":29,"value":546},{"type":23,"tag":332,"props":566,"children":567},{"style":338},[568],{"type":29,"value":569},"++",{"type":23,"tag":332,"props":571,"children":572},{"style":350},[573],{"type":29,"value":574},")",{"type":23,"tag":332,"props":576,"children":577},{"style":350},[578],{"type":29,"value":579}," {\n",{"type":23,"tag":332,"props":581,"children":583},{"class":334,"line":582},6,[584,589,593,597,602,606,610,614,619,623,628,632,636,640,644],{"type":23,"tag":332,"props":585,"children":586},{"style":509},[587],{"type":29,"value":588},"  for",{"type":23,"tag":332,"props":590,"children":591},{"style":350},[592],{"type":29,"value":517},{"type":23,"tag":332,"props":594,"children":595},{"style":338},[596],{"type":29,"value":522},{"type":23,"tag":332,"props":598,"children":599},{"style":344},[600],{"type":29,"value":601},"j",{"type":23,"tag":332,"props":603,"children":604},{"style":350},[605],{"type":29,"value":532},{"type":23,"tag":332,"props":607,"children":608},{"style":388},[609],{"type":29,"value":469},{"type":23,"tag":332,"props":611,"children":612},{"style":350},[613],{"type":29,"value":541},{"type":23,"tag":332,"props":615,"children":616},{"style":344},[617],{"type":29,"value":618}," j",{"type":23,"tag":332,"props":620,"children":621},{"style":350},[622],{"type":29,"value":551},{"type":23,"tag":332,"props":624,"children":625},{"style":344},[626],{"type":29,"value":627}," len2",{"type":23,"tag":332,"props":629,"children":630},{"style":350},[631],{"type":29,"value":541},{"type":23,"tag":332,"props":633,"children":634},{"style":344},[635],{"type":29,"value":618},{"type":23,"tag":332,"props":637,"children":638},{"style":338},[639],{"type":29,"value":569},{"type":23,"tag":332,"props":641,"children":642},{"style":350},[643],{"type":29,"value":574},{"type":23,"tag":332,"props":645,"children":646},{"style":350},[647],{"type":29,"value":579},{"type":23,"tag":332,"props":649,"children":651},{"class":334,"line":650},7,[652,657,661,666,671,675,680,684,689,694,699,703,707,711,715,720],{"type":23,"tag":332,"props":653,"children":654},{"style":509},[655],{"type":29,"value":656},"    if",{"type":23,"tag":332,"props":658,"children":659},{"style":350},[660],{"type":29,"value":517},{"type":23,"tag":332,"props":662,"children":663},{"style":344},[664],{"type":29,"value":665},"str1",{"type":23,"tag":332,"props":667,"children":668},{"style":350},[669],{"type":29,"value":670},"[",{"type":23,"tag":332,"props":672,"children":673},{"style":344},[674],{"type":29,"value":527},{"type":23,"tag":332,"props":676,"children":677},{"style":338},[678],{"type":29,"value":679}," -",{"type":23,"tag":332,"props":681,"children":682},{"style":388},[683],{"type":29,"value":469},{"type":23,"tag":332,"props":685,"children":686},{"style":350},[687],{"type":29,"value":688},"]",{"type":23,"tag":332,"props":690,"children":691},{"style":338},[692],{"type":29,"value":693}," ===",{"type":23,"tag":332,"props":695,"children":696},{"style":344},[697],{"type":29,"value":698}," str2",{"type":23,"tag":332,"props":700,"children":701},{"style":350},[702],{"type":29,"value":670},{"type":23,"tag":332,"props":704,"children":705},{"style":344},[706],{"type":29,"value":601},{"type":23,"tag":332,"props":708,"children":709},{"style":338},[710],{"type":29,"value":679},{"type":23,"tag":332,"props":712,"children":713},{"style":388},[714],{"type":29,"value":469},{"type":23,"tag":332,"props":716,"children":717},{"style":350},[718],{"type":29,"value":719},"])",{"type":23,"tag":332,"props":721,"children":722},{"style":350},[723],{"type":29,"value":579},{"type":23,"tag":332,"props":725,"children":727},{"class":334,"line":726},8,[728,733,737,741,746,750,754,758,763,767,771,775,779,783,787,791,795,799,803],{"type":23,"tag":332,"props":729,"children":730},{"style":344},[731],{"type":29,"value":732},"      lcsTable",{"type":23,"tag":332,"props":734,"children":735},{"style":350},[736],{"type":29,"value":670},{"type":23,"tag":332,"props":738,"children":739},{"style":344},[740],{"type":29,"value":527},{"type":23,"tag":332,"props":742,"children":743},{"style":350},[744],{"type":29,"value":745},"][",{"type":23,"tag":332,"props":747,"children":748},{"style":344},[749],{"type":29,"value":601},{"type":23,"tag":332,"props":751,"children":752},{"style":350},[753],{"type":29,"value":688},{"type":23,"tag":332,"props":755,"children":756},{"style":350},[757],{"type":29,"value":532},{"type":23,"tag":332,"props":759,"children":760},{"style":344},[761],{"type":29,"value":762}," lcsTable",{"type":23,"tag":332,"props":764,"children":765},{"style":350},[766],{"type":29,"value":670},{"type":23,"tag":332,"props":768,"children":769},{"style":344},[770],{"type":29,"value":527},{"type":23,"tag":332,"props":772,"children":773},{"style":338},[774],{"type":29,"value":679},{"type":23,"tag":332,"props":776,"children":777},{"style":388},[778],{"type":29,"value":469},{"type":23,"tag":332,"props":780,"children":781},{"style":350},[782],{"type":29,"value":745},{"type":23,"tag":332,"props":784,"children":785},{"style":344},[786],{"type":29,"value":601},{"type":23,"tag":332,"props":788,"children":789},{"style":338},[790],{"type":29,"value":679},{"type":23,"tag":332,"props":792,"children":793},{"style":388},[794],{"type":29,"value":469},{"type":23,"tag":332,"props":796,"children":797},{"style":350},[798],{"type":29,"value":688},{"type":23,"tag":332,"props":800,"children":801},{"style":338},[802],{"type":29,"value":464},{"type":23,"tag":332,"props":804,"children":805},{"style":388},[806],{"type":29,"value":807}," 1\n",{"type":23,"tag":332,"props":809,"children":811},{"class":334,"line":810},9,[812,817,822],{"type":23,"tag":332,"props":813,"children":814},{"style":350},[815],{"type":29,"value":816},"    }",{"type":23,"tag":332,"props":818,"children":819},{"style":509},[820],{"type":29,"value":821}," else",{"type":23,"tag":332,"props":823,"children":824},{"style":350},[825],{"type":29,"value":579},{"type":23,"tag":332,"props":827,"children":829},{"class":334,"line":828},10,[830,834,838,842,846,850,854,858,863,868,873,877,881,885,889,893,897,901,905,910,914,918,922,926,930,934,938],{"type":23,"tag":332,"props":831,"children":832},{"style":344},[833],{"type":29,"value":732},{"type":23,"tag":332,"props":835,"children":836},{"style":350},[837],{"type":29,"value":670},{"type":23,"tag":332,"props":839,"children":840},{"style":344},[841],{"type":29,"value":527},{"type":23,"tag":332,"props":843,"children":844},{"style":350},[845],{"type":29,"value":745},{"type":23,"tag":332,"props":847,"children":848},{"style":344},[849],{"type":29,"value":601},{"type":23,"tag":332,"props":851,"children":852},{"style":350},[853],{"type":29,"value":688},{"type":23,"tag":332,"props":855,"children":856},{"style":350},[857],{"type":29,"value":532},{"type":23,"tag":332,"props":859,"children":860},{"style":344},[861],{"type":29,"value":862}," Math",{"type":23,"tag":332,"props":864,"children":865},{"style":350},[866],{"type":29,"value":867},".",{"type":23,"tag":332,"props":869,"children":870},{"style":367},[871],{"type":29,"value":872},"max",{"type":23,"tag":332,"props":874,"children":875},{"style":350},[876],{"type":29,"value":375},{"type":23,"tag":332,"props":878,"children":879},{"style":344},[880],{"type":29,"value":347},{"type":23,"tag":332,"props":882,"children":883},{"style":350},[884],{"type":29,"value":670},{"type":23,"tag":332,"props":886,"children":887},{"style":344},[888],{"type":29,"value":527},{"type":23,"tag":332,"props":890,"children":891},{"style":338},[892],{"type":29,"value":679},{"type":23,"tag":332,"props":894,"children":895},{"style":388},[896],{"type":29,"value":469},{"type":23,"tag":332,"props":898,"children":899},{"style":350},[900],{"type":29,"value":745},{"type":23,"tag":332,"props":902,"children":903},{"style":344},[904],{"type":29,"value":601},{"type":23,"tag":332,"props":906,"children":907},{"style":350},[908],{"type":29,"value":909},"],",{"type":23,"tag":332,"props":911,"children":912},{"style":344},[913],{"type":29,"value":762},{"type":23,"tag":332,"props":915,"children":916},{"style":350},[917],{"type":29,"value":670},{"type":23,"tag":332,"props":919,"children":920},{"style":344},[921],{"type":29,"value":527},{"type":23,"tag":332,"props":923,"children":924},{"style":350},[925],{"type":29,"value":745},{"type":23,"tag":332,"props":927,"children":928},{"style":344},[929],{"type":29,"value":601},{"type":23,"tag":332,"props":931,"children":932},{"style":338},[933],{"type":29,"value":679},{"type":23,"tag":332,"props":935,"children":936},{"style":388},[937],{"type":29,"value":469},{"type":23,"tag":332,"props":939,"children":940},{"style":350},[941],{"type":29,"value":942},"])\n",{"type":23,"tag":332,"props":944,"children":946},{"class":334,"line":945},11,[947],{"type":23,"tag":332,"props":948,"children":949},{"style":350},[950],{"type":29,"value":951},"    }\n",{"type":23,"tag":332,"props":953,"children":955},{"class":334,"line":954},12,[956],{"type":23,"tag":332,"props":957,"children":958},{"style":350},[959],{"type":29,"value":960},"  }\n",{"type":23,"tag":332,"props":962,"children":964},{"class":334,"line":963},13,[965],{"type":23,"tag":332,"props":966,"children":967},{"style":350},[968],{"type":29,"value":969},"}\n",{"type":23,"tag":31,"props":971,"children":972},{},[973,975,980],{"type":29,"value":974},"この表が埋まったら、右下から左上へ ",{"type":23,"tag":303,"props":976,"children":977},{},[978],{"type":29,"value":979},"バックトレース",{"type":29,"value":981}," することで「どこが一致で、どこが追加/削除か」の操作列を取り出せます。計算量は O(m·n) ですが、対象サイズが小さいことを制約としているので、十分なはずです。小規模ツールの設計では制約決めが重要だという話はここでも活きてきます。",{"type":23,"tag":983,"props":984,"children":987},"link-card",{"label":985,"to":986},"軽量ツールの制約設計についてはこちら⬇️","/articles/tech/development/yomogi-constraint-driven-design",[],{"type":23,"tag":24,"props":989,"children":991},{"id":990},"二段構成行レベル-文字レベル",[992],{"type":29,"value":993},"二段構成：行レベル → 文字レベル",{"type":23,"tag":31,"props":995,"children":996},{},[997,999,1004],{"type":29,"value":998},"差分比較ツールの面白いところは、",{"type":23,"tag":303,"props":1000,"children":1001},{},[1002],{"type":29,"value":1003},"同じ LCS ロジックを二段階で使う",{"type":29,"value":1005}," 点です。\n行単位の差分で大まかに分類し、さらに文字単位で差分を作るという仕組みになります。",{"type":23,"tag":158,"props":1007,"children":1009},{"id":1008},"第1段行レベルの差分",[1010],{"type":29,"value":1011},"第1段：行レベルの差分",{"type":23,"tag":31,"props":1013,"children":1014},{},[1015,1017,1023,1025,1031],{"type":29,"value":1016},"まずテキストを ",{"type":23,"tag":59,"props":1018,"children":1020},{"className":1019},[],[1021],{"type":29,"value":1022},"\\n",{"type":29,"value":1024}," で分割して行の配列を作り、行同士を要素比較します。等価判定は行文字列の完全一致。",{"type":23,"tag":59,"props":1026,"children":1028},{"className":1027},[],[1029],{"type":29,"value":1030},"findLineDiff",{"type":29,"value":1032}," がこの役割を担っています。",{"type":23,"tag":322,"props":1034,"children":1036},{"className":324,"code":1035,"language":326,"meta":7,"style":7},"if (lines1[i - 1] === lines2[j - 1]) {\n  lcsTable[i][j] = lcsTable[i - 1][j - 1] + 1\n} else {\n  lcsTable[i][j] = Math.max(lcsTable[i - 1][j], lcsTable[i][j - 1])\n}\n",[1037],{"type":23,"tag":59,"props":1038,"children":1039},{"__ignoreMap":7},[1040,1110,1190,1206,1317],{"type":23,"tag":332,"props":1041,"children":1042},{"class":334,"line":17},[1043,1048,1052,1057,1061,1065,1069,1073,1077,1081,1086,1090,1094,1098,1102,1106],{"type":23,"tag":332,"props":1044,"children":1045},{"style":509},[1046],{"type":29,"value":1047},"if",{"type":23,"tag":332,"props":1049,"children":1050},{"style":350},[1051],{"type":29,"value":517},{"type":23,"tag":332,"props":1053,"children":1054},{"style":344},[1055],{"type":29,"value":1056},"lines1",{"type":23,"tag":332,"props":1058,"children":1059},{"style":350},[1060],{"type":29,"value":670},{"type":23,"tag":332,"props":1062,"children":1063},{"style":344},[1064],{"type":29,"value":527},{"type":23,"tag":332,"props":1066,"children":1067},{"style":338},[1068],{"type":29,"value":679},{"type":23,"tag":332,"props":1070,"children":1071},{"style":388},[1072],{"type":29,"value":469},{"type":23,"tag":332,"props":1074,"children":1075},{"style":350},[1076],{"type":29,"value":688},{"type":23,"tag":332,"props":1078,"children":1079},{"style":338},[1080],{"type":29,"value":693},{"type":23,"tag":332,"props":1082,"children":1083},{"style":344},[1084],{"type":29,"value":1085}," lines2",{"type":23,"tag":332,"props":1087,"children":1088},{"style":350},[1089],{"type":29,"value":670},{"type":23,"tag":332,"props":1091,"children":1092},{"style":344},[1093],{"type":29,"value":601},{"type":23,"tag":332,"props":1095,"children":1096},{"style":338},[1097],{"type":29,"value":679},{"type":23,"tag":332,"props":1099,"children":1100},{"style":388},[1101],{"type":29,"value":469},{"type":23,"tag":332,"props":1103,"children":1104},{"style":350},[1105],{"type":29,"value":719},{"type":23,"tag":332,"props":1107,"children":1108},{"style":350},[1109],{"type":29,"value":579},{"type":23,"tag":332,"props":1111,"children":1112},{"class":334,"line":399},[1113,1118,1122,1126,1130,1134,1138,1142,1146,1150,1154,1158,1162,1166,1170,1174,1178,1182,1186],{"type":23,"tag":332,"props":1114,"children":1115},{"style":344},[1116],{"type":29,"value":1117},"  lcsTable",{"type":23,"tag":332,"props":1119,"children":1120},{"style":350},[1121],{"type":29,"value":670},{"type":23,"tag":332,"props":1123,"children":1124},{"style":344},[1125],{"type":29,"value":527},{"type":23,"tag":332,"props":1127,"children":1128},{"style":350},[1129],{"type":29,"value":745},{"type":23,"tag":332,"props":1131,"children":1132},{"style":344},[1133],{"type":29,"value":601},{"type":23,"tag":332,"props":1135,"children":1136},{"style":350},[1137],{"type":29,"value":688},{"type":23,"tag":332,"props":1139,"children":1140},{"style":350},[1141],{"type":29,"value":532},{"type":23,"tag":332,"props":1143,"children":1144},{"style":344},[1145],{"type":29,"value":762},{"type":23,"tag":332,"props":1147,"children":1148},{"style":350},[1149],{"type":29,"value":670},{"type":23,"tag":332,"props":1151,"children":1152},{"style":344},[1153],{"type":29,"value":527},{"type":23,"tag":332,"props":1155,"children":1156},{"style":338},[1157],{"type":29,"value":679},{"type":23,"tag":332,"props":1159,"children":1160},{"style":388},[1161],{"type":29,"value":469},{"type":23,"tag":332,"props":1163,"children":1164},{"style":350},[1165],{"type":29,"value":745},{"type":23,"tag":332,"props":1167,"children":1168},{"style":344},[1169],{"type":29,"value":601},{"type":23,"tag":332,"props":1171,"children":1172},{"style":338},[1173],{"type":29,"value":679},{"type":23,"tag":332,"props":1175,"children":1176},{"style":388},[1177],{"type":29,"value":469},{"type":23,"tag":332,"props":1179,"children":1180},{"style":350},[1181],{"type":29,"value":688},{"type":23,"tag":332,"props":1183,"children":1184},{"style":338},[1185],{"type":29,"value":464},{"type":23,"tag":332,"props":1187,"children":1188},{"style":388},[1189],{"type":29,"value":807},{"type":23,"tag":332,"props":1191,"children":1192},{"class":334,"line":426},[1193,1198,1202],{"type":23,"tag":332,"props":1194,"children":1195},{"style":350},[1196],{"type":29,"value":1197},"}",{"type":23,"tag":332,"props":1199,"children":1200},{"style":509},[1201],{"type":29,"value":821},{"type":23,"tag":332,"props":1203,"children":1204},{"style":350},[1205],{"type":29,"value":579},{"type":23,"tag":332,"props":1207,"children":1208},{"class":334,"line":495},[1209,1213,1217,1221,1225,1229,1233,1237,1241,1245,1249,1253,1257,1261,1265,1269,1273,1277,1281,1285,1289,1293,1297,1301,1305,1309,1313],{"type":23,"tag":332,"props":1210,"children":1211},{"style":344},[1212],{"type":29,"value":1117},{"type":23,"tag":332,"props":1214,"children":1215},{"style":350},[1216],{"type":29,"value":670},{"type":23,"tag":332,"props":1218,"children":1219},{"style":344},[1220],{"type":29,"value":527},{"type":23,"tag":332,"props":1222,"children":1223},{"style":350},[1224],{"type":29,"value":745},{"type":23,"tag":332,"props":1226,"children":1227},{"style":344},[1228],{"type":29,"value":601},{"type":23,"tag":332,"props":1230,"children":1231},{"style":350},[1232],{"type":29,"value":688},{"type":23,"tag":332,"props":1234,"children":1235},{"style":350},[1236],{"type":29,"value":532},{"type":23,"tag":332,"props":1238,"children":1239},{"style":344},[1240],{"type":29,"value":862},{"type":23,"tag":332,"props":1242,"children":1243},{"style":350},[1244],{"type":29,"value":867},{"type":23,"tag":332,"props":1246,"children":1247},{"style":367},[1248],{"type":29,"value":872},{"type":23,"tag":332,"props":1250,"children":1251},{"style":350},[1252],{"type":29,"value":375},{"type":23,"tag":332,"props":1254,"children":1255},{"style":344},[1256],{"type":29,"value":347},{"type":23,"tag":332,"props":1258,"children":1259},{"style":350},[1260],{"type":29,"value":670},{"type":23,"tag":332,"props":1262,"children":1263},{"style":344},[1264],{"type":29,"value":527},{"type":23,"tag":332,"props":1266,"children":1267},{"style":338},[1268],{"type":29,"value":679},{"type":23,"tag":332,"props":1270,"children":1271},{"style":388},[1272],{"type":29,"value":469},{"type":23,"tag":332,"props":1274,"children":1275},{"style":350},[1276],{"type":29,"value":745},{"type":23,"tag":332,"props":1278,"children":1279},{"style":344},[1280],{"type":29,"value":601},{"type":23,"tag":332,"props":1282,"children":1283},{"style":350},[1284],{"type":29,"value":909},{"type":23,"tag":332,"props":1286,"children":1287},{"style":344},[1288],{"type":29,"value":762},{"type":23,"tag":332,"props":1290,"children":1291},{"style":350},[1292],{"type":29,"value":670},{"type":23,"tag":332,"props":1294,"children":1295},{"style":344},[1296],{"type":29,"value":527},{"type":23,"tag":332,"props":1298,"children":1299},{"style":350},[1300],{"type":29,"value":745},{"type":23,"tag":332,"props":1302,"children":1303},{"style":344},[1304],{"type":29,"value":601},{"type":23,"tag":332,"props":1306,"children":1307},{"style":338},[1308],{"type":29,"value":679},{"type":23,"tag":332,"props":1310,"children":1311},{"style":388},[1312],{"type":29,"value":469},{"type":23,"tag":332,"props":1314,"children":1315},{"style":350},[1316],{"type":29,"value":942},{"type":23,"tag":332,"props":1318,"children":1319},{"class":334,"line":505},[1320],{"type":23,"tag":332,"props":1321,"children":1322},{"style":350},[1323],{"type":29,"value":969},{"type":23,"tag":31,"props":1325,"children":1326},{},[1327,1329,1335,1337,1343],{"type":29,"value":1328},"一致しない行は ",{"type":23,"tag":59,"props":1330,"children":1332},{"className":1331},[],[1333],{"type":29,"value":1334},"removed",{"type":29,"value":1336}," / ",{"type":23,"tag":59,"props":1338,"children":1340},{"className":1339},[],[1341],{"type":29,"value":1342},"added",{"type":29,"value":1344}," として扱われます。この時点では「行ごと」の粒度しか分かりません。",{"type":23,"tag":158,"props":1346,"children":1348},{"id":1347},"第2段changed-ペアを文字レベルで再差分",[1349],{"type":29,"value":1350},"第2段：\"changed\" ペアを文字レベルで再差分",{"type":23,"tag":31,"props":1352,"children":1353},{},[1354,1356,1361,1363,1368,1370,1375,1377,1382,1384,1390,1392,1397],{"type":29,"value":1355},"行レベルで ",{"type":23,"tag":59,"props":1357,"children":1359},{"className":1358},[],[1360],{"type":29,"value":1334},{"type":29,"value":1362}," の直後に ",{"type":23,"tag":59,"props":1364,"children":1366},{"className":1365},[],[1367],{"type":29,"value":1342},{"type":29,"value":1369}," が来た場合、「",{"type":23,"tag":303,"props":1371,"children":1372},{},[1373],{"type":29,"value":1374},"丸ごと別の行に置き換わった",{"type":29,"value":1376},"」のではなく「",{"type":23,"tag":303,"props":1378,"children":1379},{},[1380],{"type":29,"value":1381},"少しだけ編集された",{"type":29,"value":1383},"」可能性が高いです。そこで差分比較ツールでは隣接する ",{"type":23,"tag":59,"props":1385,"children":1387},{"className":1386},[],[1388],{"type":29,"value":1389},"removed + added",{"type":29,"value":1391}," ペアを ",{"type":23,"tag":59,"props":1393,"children":1395},{"className":1394},[],[1396],{"type":29,"value":64},{"type":29,"value":1398}," にします。",{"type":23,"tag":322,"props":1400,"children":1402},{"className":324,"code":1401,"language":326,"meta":7,"style":7},"while (idx \u003C ops.length) {\n  if (\n    idx + 1 \u003C ops.length &&\n    ops[idx].type === 'removed' &&\n    ops[idx + 1].type === 'added'\n  ) {\n    merged.push({\n      type: 'changed',\n      originalLine: (ops[idx] as { line: string }).line,\n      modifiedLine: (ops[idx + 1] as { line: string }).line,\n    })\n    idx += 2\n  } else {\n    merged.push(ops[idx])\n    idx++\n  }\n}\n",[1403],{"type":23,"tag":59,"props":1404,"children":1405},{"__ignoreMap":7},[1406,1451,1464,1501,1551,1599,1611,1633,1662,1729,1797,1805,1822,1838,1874,1887,1895],{"type":23,"tag":332,"props":1407,"children":1408},{"class":334,"line":17},[1409,1414,1418,1423,1428,1433,1437,1443,1447],{"type":23,"tag":332,"props":1410,"children":1411},{"style":509},[1412],{"type":29,"value":1413},"while",{"type":23,"tag":332,"props":1415,"children":1416},{"style":350},[1417],{"type":29,"value":517},{"type":23,"tag":332,"props":1419,"children":1420},{"style":344},[1421],{"type":29,"value":1422},"idx",{"type":23,"tag":332,"props":1424,"children":1425},{"style":350},[1426],{"type":29,"value":1427}," \u003C",{"type":23,"tag":332,"props":1429,"children":1430},{"style":344},[1431],{"type":29,"value":1432}," ops",{"type":23,"tag":332,"props":1434,"children":1435},{"style":350},[1436],{"type":29,"value":867},{"type":23,"tag":332,"props":1438,"children":1440},{"style":1439},"--shiki-default:#B8A965",[1441],{"type":29,"value":1442},"length",{"type":23,"tag":332,"props":1444,"children":1445},{"style":350},[1446],{"type":29,"value":574},{"type":23,"tag":332,"props":1448,"children":1449},{"style":350},[1450],{"type":29,"value":579},{"type":23,"tag":332,"props":1452,"children":1453},{"class":334,"line":399},[1454,1459],{"type":23,"tag":332,"props":1455,"children":1456},{"style":509},[1457],{"type":29,"value":1458},"  if",{"type":23,"tag":332,"props":1460,"children":1461},{"style":350},[1462],{"type":29,"value":1463}," (\n",{"type":23,"tag":332,"props":1465,"children":1466},{"class":334,"line":426},[1467,1472,1476,1480,1484,1488,1492,1496],{"type":23,"tag":332,"props":1468,"children":1469},{"style":344},[1470],{"type":29,"value":1471},"    idx",{"type":23,"tag":332,"props":1473,"children":1474},{"style":338},[1475],{"type":29,"value":464},{"type":23,"tag":332,"props":1477,"children":1478},{"style":388},[1479],{"type":29,"value":469},{"type":23,"tag":332,"props":1481,"children":1482},{"style":350},[1483],{"type":29,"value":1427},{"type":23,"tag":332,"props":1485,"children":1486},{"style":344},[1487],{"type":29,"value":1432},{"type":23,"tag":332,"props":1489,"children":1490},{"style":350},[1491],{"type":29,"value":867},{"type":23,"tag":332,"props":1493,"children":1494},{"style":1439},[1495],{"type":29,"value":1442},{"type":23,"tag":332,"props":1497,"children":1498},{"style":338},[1499],{"type":29,"value":1500}," &&\n",{"type":23,"tag":332,"props":1502,"children":1503},{"class":334,"line":495},[1504,1509,1513,1517,1522,1527,1531,1537,1542,1547],{"type":23,"tag":332,"props":1505,"children":1506},{"style":344},[1507],{"type":29,"value":1508},"    ops",{"type":23,"tag":332,"props":1510,"children":1511},{"style":350},[1512],{"type":29,"value":670},{"type":23,"tag":332,"props":1514,"children":1515},{"style":344},[1516],{"type":29,"value":1422},{"type":23,"tag":332,"props":1518,"children":1519},{"style":350},[1520],{"type":29,"value":1521},"].",{"type":23,"tag":332,"props":1523,"children":1524},{"style":344},[1525],{"type":29,"value":1526},"type",{"type":23,"tag":332,"props":1528,"children":1529},{"style":338},[1530],{"type":29,"value":693},{"type":23,"tag":332,"props":1532,"children":1534},{"style":1533},"--shiki-default:#C98A7D77",[1535],{"type":29,"value":1536}," '",{"type":23,"tag":332,"props":1538,"children":1540},{"style":1539},"--shiki-default:#C98A7D",[1541],{"type":29,"value":1334},{"type":23,"tag":332,"props":1543,"children":1544},{"style":1533},[1545],{"type":29,"value":1546},"'",{"type":23,"tag":332,"props":1548,"children":1549},{"style":338},[1550],{"type":29,"value":1500},{"type":23,"tag":332,"props":1552,"children":1553},{"class":334,"line":505},[1554,1558,1562,1566,1570,1574,1578,1582,1586,1590,1594],{"type":23,"tag":332,"props":1555,"children":1556},{"style":344},[1557],{"type":29,"value":1508},{"type":23,"tag":332,"props":1559,"children":1560},{"style":350},[1561],{"type":29,"value":670},{"type":23,"tag":332,"props":1563,"children":1564},{"style":344},[1565],{"type":29,"value":1422},{"type":23,"tag":332,"props":1567,"children":1568},{"style":338},[1569],{"type":29,"value":464},{"type":23,"tag":332,"props":1571,"children":1572},{"style":388},[1573],{"type":29,"value":469},{"type":23,"tag":332,"props":1575,"children":1576},{"style":350},[1577],{"type":29,"value":1521},{"type":23,"tag":332,"props":1579,"children":1580},{"style":344},[1581],{"type":29,"value":1526},{"type":23,"tag":332,"props":1583,"children":1584},{"style":338},[1585],{"type":29,"value":693},{"type":23,"tag":332,"props":1587,"children":1588},{"style":1533},[1589],{"type":29,"value":1536},{"type":23,"tag":332,"props":1591,"children":1592},{"style":1539},[1593],{"type":29,"value":1342},{"type":23,"tag":332,"props":1595,"children":1596},{"style":1533},[1597],{"type":29,"value":1598},"'\n",{"type":23,"tag":332,"props":1600,"children":1601},{"class":334,"line":582},[1602,1607],{"type":23,"tag":332,"props":1603,"children":1604},{"style":350},[1605],{"type":29,"value":1606},"  )",{"type":23,"tag":332,"props":1608,"children":1609},{"style":350},[1610],{"type":29,"value":579},{"type":23,"tag":332,"props":1612,"children":1613},{"class":334,"line":650},[1614,1619,1623,1628],{"type":23,"tag":332,"props":1615,"children":1616},{"style":344},[1617],{"type":29,"value":1618},"    merged",{"type":23,"tag":332,"props":1620,"children":1621},{"style":350},[1622],{"type":29,"value":867},{"type":23,"tag":332,"props":1624,"children":1625},{"style":367},[1626],{"type":29,"value":1627},"push",{"type":23,"tag":332,"props":1629,"children":1630},{"style":350},[1631],{"type":29,"value":1632},"({\n",{"type":23,"tag":332,"props":1634,"children":1635},{"class":334,"line":726},[1636,1641,1645,1649,1653,1657],{"type":23,"tag":332,"props":1637,"children":1638},{"style":1439},[1639],{"type":29,"value":1640},"      type",{"type":23,"tag":332,"props":1642,"children":1643},{"style":350},[1644],{"type":29,"value":353},{"type":23,"tag":332,"props":1646,"children":1647},{"style":1533},[1648],{"type":29,"value":1546},{"type":23,"tag":332,"props":1650,"children":1651},{"style":1539},[1652],{"type":29,"value":64},{"type":23,"tag":332,"props":1654,"children":1655},{"style":1533},[1656],{"type":29,"value":1546},{"type":23,"tag":332,"props":1658,"children":1659},{"style":350},[1660],{"type":29,"value":1661},",\n",{"type":23,"tag":332,"props":1663,"children":1664},{"class":334,"line":810},[1665,1670,1675,1680,1684,1688,1693,1698,1703,1707,1711,1716,1721,1725],{"type":23,"tag":332,"props":1666,"children":1667},{"style":1439},[1668],{"type":29,"value":1669},"      originalLine",{"type":23,"tag":332,"props":1671,"children":1672},{"style":350},[1673],{"type":29,"value":1674},": (",{"type":23,"tag":332,"props":1676,"children":1677},{"style":344},[1678],{"type":29,"value":1679},"ops",{"type":23,"tag":332,"props":1681,"children":1682},{"style":350},[1683],{"type":29,"value":670},{"type":23,"tag":332,"props":1685,"children":1686},{"style":344},[1687],{"type":29,"value":1422},{"type":23,"tag":332,"props":1689,"children":1690},{"style":350},[1691],{"type":29,"value":1692},"] ",{"type":23,"tag":332,"props":1694,"children":1695},{"style":509},[1696],{"type":29,"value":1697},"as",{"type":23,"tag":332,"props":1699,"children":1700},{"style":350},[1701],{"type":29,"value":1702}," { ",{"type":23,"tag":332,"props":1704,"children":1705},{"style":344},[1706],{"type":29,"value":334},{"type":23,"tag":332,"props":1708,"children":1709},{"style":350},[1710],{"type":29,"value":353},{"type":23,"tag":332,"props":1712,"children":1713},{"style":356},[1714],{"type":29,"value":1715},"string",{"type":23,"tag":332,"props":1717,"children":1718},{"style":350},[1719],{"type":29,"value":1720}," }).",{"type":23,"tag":332,"props":1722,"children":1723},{"style":344},[1724],{"type":29,"value":334},{"type":23,"tag":332,"props":1726,"children":1727},{"style":350},[1728],{"type":29,"value":1661},{"type":23,"tag":332,"props":1730,"children":1731},{"class":334,"line":828},[1732,1737,1741,1745,1749,1753,1757,1761,1765,1769,1773,1777,1781,1785,1789,1793],{"type":23,"tag":332,"props":1733,"children":1734},{"style":1439},[1735],{"type":29,"value":1736},"      modifiedLine",{"type":23,"tag":332,"props":1738,"children":1739},{"style":350},[1740],{"type":29,"value":1674},{"type":23,"tag":332,"props":1742,"children":1743},{"style":344},[1744],{"type":29,"value":1679},{"type":23,"tag":332,"props":1746,"children":1747},{"style":350},[1748],{"type":29,"value":670},{"type":23,"tag":332,"props":1750,"children":1751},{"style":344},[1752],{"type":29,"value":1422},{"type":23,"tag":332,"props":1754,"children":1755},{"style":338},[1756],{"type":29,"value":464},{"type":23,"tag":332,"props":1758,"children":1759},{"style":388},[1760],{"type":29,"value":469},{"type":23,"tag":332,"props":1762,"children":1763},{"style":350},[1764],{"type":29,"value":1692},{"type":23,"tag":332,"props":1766,"children":1767},{"style":509},[1768],{"type":29,"value":1697},{"type":23,"tag":332,"props":1770,"children":1771},{"style":350},[1772],{"type":29,"value":1702},{"type":23,"tag":332,"props":1774,"children":1775},{"style":344},[1776],{"type":29,"value":334},{"type":23,"tag":332,"props":1778,"children":1779},{"style":350},[1780],{"type":29,"value":353},{"type":23,"tag":332,"props":1782,"children":1783},{"style":356},[1784],{"type":29,"value":1715},{"type":23,"tag":332,"props":1786,"children":1787},{"style":350},[1788],{"type":29,"value":1720},{"type":23,"tag":332,"props":1790,"children":1791},{"style":344},[1792],{"type":29,"value":334},{"type":23,"tag":332,"props":1794,"children":1795},{"style":350},[1796],{"type":29,"value":1661},{"type":23,"tag":332,"props":1798,"children":1799},{"class":334,"line":945},[1800],{"type":23,"tag":332,"props":1801,"children":1802},{"style":350},[1803],{"type":29,"value":1804},"    })\n",{"type":23,"tag":332,"props":1806,"children":1807},{"class":334,"line":954},[1808,1812,1817],{"type":23,"tag":332,"props":1809,"children":1810},{"style":344},[1811],{"type":29,"value":1471},{"type":23,"tag":332,"props":1813,"children":1814},{"style":338},[1815],{"type":29,"value":1816}," +=",{"type":23,"tag":332,"props":1818,"children":1819},{"style":388},[1820],{"type":29,"value":1821}," 2\n",{"type":23,"tag":332,"props":1823,"children":1824},{"class":334,"line":963},[1825,1830,1834],{"type":23,"tag":332,"props":1826,"children":1827},{"style":350},[1828],{"type":29,"value":1829},"  }",{"type":23,"tag":332,"props":1831,"children":1832},{"style":509},[1833],{"type":29,"value":821},{"type":23,"tag":332,"props":1835,"children":1836},{"style":350},[1837],{"type":29,"value":579},{"type":23,"tag":332,"props":1839,"children":1841},{"class":334,"line":1840},14,[1842,1846,1850,1854,1858,1862,1866,1870],{"type":23,"tag":332,"props":1843,"children":1844},{"style":344},[1845],{"type":29,"value":1618},{"type":23,"tag":332,"props":1847,"children":1848},{"style":350},[1849],{"type":29,"value":867},{"type":23,"tag":332,"props":1851,"children":1852},{"style":367},[1853],{"type":29,"value":1627},{"type":23,"tag":332,"props":1855,"children":1856},{"style":350},[1857],{"type":29,"value":375},{"type":23,"tag":332,"props":1859,"children":1860},{"style":344},[1861],{"type":29,"value":1679},{"type":23,"tag":332,"props":1863,"children":1864},{"style":350},[1865],{"type":29,"value":670},{"type":23,"tag":332,"props":1867,"children":1868},{"style":344},[1869],{"type":29,"value":1422},{"type":23,"tag":332,"props":1871,"children":1872},{"style":350},[1873],{"type":29,"value":942},{"type":23,"tag":332,"props":1875,"children":1877},{"class":334,"line":1876},15,[1878,1882],{"type":23,"tag":332,"props":1879,"children":1880},{"style":344},[1881],{"type":29,"value":1471},{"type":23,"tag":332,"props":1883,"children":1884},{"style":338},[1885],{"type":29,"value":1886},"++\n",{"type":23,"tag":332,"props":1888,"children":1890},{"class":334,"line":1889},16,[1891],{"type":23,"tag":332,"props":1892,"children":1893},{"style":350},[1894],{"type":29,"value":960},{"type":23,"tag":332,"props":1896,"children":1898},{"class":334,"line":1897},17,[1899],{"type":23,"tag":332,"props":1900,"children":1901},{"style":350},[1902],{"type":29,"value":969},{"type":23,"tag":31,"props":1904,"children":1905},{},[1906,1908,1913,1915,1920,1922,1928,1930,1936],{"type":29,"value":1907},"そして ",{"type":23,"tag":59,"props":1909,"children":1911},{"className":1910},[],[1912],{"type":29,"value":64},{"type":29,"value":1914}," な行ペアについては、",{"type":23,"tag":303,"props":1916,"children":1917},{},[1918],{"type":29,"value":1919},"文字単位の LCS",{"type":29,"value":1921}," をもう一度かけて、行内のどこが変わったかを細かくハイライトします。これを担うのが ",{"type":23,"tag":59,"props":1923,"children":1925},{"className":1924},[],[1926],{"type":29,"value":1927},"buildCharSegments",{"type":29,"value":1929}," で、実態は ",{"type":23,"tag":59,"props":1931,"children":1933},{"className":1932},[],[1934],{"type":29,"value":1935},"findCharacterDiff",{"type":29,"value":1937},"（先ほど見せたのと同じ LCS + DP）です。",{"type":23,"tag":31,"props":1939,"children":1940},{},[1941,1943,1948],{"type":29,"value":1942},"行レベルと文字レベルで ",{"type":23,"tag":303,"props":1944,"children":1945},{},[1946],{"type":29,"value":1947},"まったく同じ関数ひな形を使い回せる",{"type":29,"value":1949}," のが、この設計の特徴です。",{"type":23,"tag":24,"props":1951,"children":1953},{"id":1952},"現実的な制限",[1954],{"type":29,"value":1952},{"type":23,"tag":31,"props":1956,"children":1957},{},[1958],{"type":29,"value":1959},"LCS + DP は O(m·n) メモリを食うので、超長文（数十万文字）に投げると当然重くなります。差分比較ツールでは大容量テキスト向けの Web Worker 対応は未実装で、代わりにユーザー向けマニュアルで分割比較の利用を案内しています。差分計算自体はブラウザ側で同期実行していますが、100KB 程度までなら UI スレッドでも体感上気になりません。",{"type":23,"tag":31,"props":1961,"children":1962},{},[1963],{"type":29,"value":1964},"学習目的で書いた単純な実装ですが、当初想定していたユースケースでは十分に動きます。もし将来、もっと大規模なテキストを扱いたくなったら、より洗練された手法に置き換える余地もあります。",{"type":23,"tag":24,"props":1966,"children":1968},{"id":1967},"まとめ",[1969],{"type":29,"value":1967},{"type":23,"tag":181,"props":1971,"children":1972},{},[1973,1985,2004],{"type":23,"tag":185,"props":1974,"children":1975},{},[1976,1978,1983],{"type":29,"value":1977},"大学で習った ",{"type":23,"tag":303,"props":1979,"children":1980},{},[1981],{"type":29,"value":1982},"動的計画法を実用ツールに落とし込む題材",{"type":29,"value":1984}," として、差分計算は手触りがちょうど良かった",{"type":23,"tag":185,"props":1986,"children":1987},{},[1988,1990,1995,1997,2002],{"type":29,"value":1989},"行レベル LCS → ",{"type":23,"tag":59,"props":1991,"children":1993},{"className":1992},[],[1994],{"type":29,"value":64},{"type":29,"value":1996}," ペアに対して文字レベル LCS、という ",{"type":23,"tag":303,"props":1998,"children":1999},{},[2000],{"type":29,"value":2001},"二段構成",{"type":29,"value":2003}," で精度を稼ぐ",{"type":23,"tag":185,"props":2005,"children":2006},{},[2007],{"type":29,"value":2008},"超長文は対象外、と割り切ってマニュアルで案内するのも立派な設計判断",{"type":23,"tag":31,"props":2010,"children":2011},{},[2012],{"type":29,"value":2013},"学んだ手法をまずは素直に実装してみる経験は、後でより洗練された手法を読むときの理解の土台にもなると考えています。",{"type":23,"tag":2015,"props":2016,"children":2017},"style",{},[2018],{"type":29,"value":2019},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":399,"depth":399,"links":2021},[2022,2023,2024,2028,2029,2033,2034],{"id":26,"depth":399,"text":26},{"id":69,"depth":399,"text":69},{"id":153,"depth":399,"text":156,"children":2025},[2026,2027],{"id":160,"depth":426,"text":160},{"id":271,"depth":426,"text":271},{"id":312,"depth":399,"text":315},{"id":990,"depth":399,"text":993,"children":2030},[2031,2032],{"id":1008,"depth":426,"text":1011},{"id":1347,"depth":426,"text":1350},{"id":1952,"depth":399,"text":1952},{"id":1967,"depth":399,"text":1967},"markdown","content:articles:tech:development:diff-lcs-two-level.md","content","articles/tech/development/diff-lcs-two-level.md","articles/tech/development/diff-lcs-two-level","md",{"title":2042,"description":2043},"軽量なツールの設計は「何をしないか」から始まる — Yomogi の具体例で考える","個人開発の軽量ツールでは、何を「できなくするか」を決めることが設計の本質になります。ドット絵エディタ Yomogi で実際に設けた制約と、その判断基準を具体的にまとめます。",1777471288897]