阿猫的博客

阿猫的博客

Colf 题解

6
2025-09-29

Colf 是一个面向 AI prompt 的 code golf,大概就是利用最少的代码(在这里是最少的 token 数)来解题。

使用起来很简单,在左边的框中输入 prompt,生成代码,然后会自动运行是否 AC,如果 AC 则按照输入和输出 token 数计分。

小小尝试了一下,AC 完之后排名还挺前,所以写篇文章记录一下题解和心得。

题解

#1 LRU Cache

function lruops(
  ops: [op: string, key: number, value?: number][],
  capacity: number,
): number[]

code golf
function lruops(o,c){let m=new Map(),r=[];for(let [op,k,v]of o){if(op=="get"){if(m.has(k)){r.push(m.get(k)),m.delete(k),m.set(k,r[r.length-1])}else r.push(-1)}else{if(m.size==c)m.delete(m.keys().next().value);m.set(k,v)}}return r}
Input 33 tokens, Output 85 tokens, Total 118 tokens

上来先展示一下流氓解法:复制函数签名,加上 code golf。

#2 Topological Sort with Cycle Detection

function toposort(
  n: number,
  edges: [u: number, v: number][],
): number[]

Code golf choose the smallest index
function toposort(n, edges) {
    const g = Array.from({ length: n }, () => []), inDeg = Array(n).fill(0);
    edges.forEach(([u, v]) => (g[u].push(v), inDeg[v]++));
    const q = [], res = [];
    for (let i = 0; i < n; i++) if (!inDeg[i]) q.push(i);
    while (q.length) {
        const u = q.splice(q.indexOf(Math.min(...q)), 1)[0];
        res.push(u);
        for (const v of g[u]) {
            if (--inDeg[v] === 0) q.push(v);
        }
    }
    return res.length === n ? res : [];
}
Input 33 tokens, Output 159 tokens, Total 192 tokens

同样是流氓解法,但是有 case 过不了,再审题干,要选择更小的 index,所以补一句。

#3 Longest Common Subsequence

code golf
function lcs(a,b: string): string
function lcs(a,b){let dp=Array.from({length:a.length+1},()=>Array(b.length+1).fill(0));for(let i=1;i<=a.length;i++)for(let j=1;j<=b.length;j++)dp[i][j]=a[i-1]==b[j-1]?dp[i-1][j-1]+1:Math.max(dp[i-1][j],dp[i][j-1]);let res='';for(let i=a.length,j=b.length;i&&j;){if(a[i-1]==b[j-1]){res=a[i-1]+res;i--;j--;}else dp[i][j-1]>dp[i-1][j]?j--:i--;}return res;}

Input 12 tokens, Output 157 tokens, Total 169 tokens

同样的流氓解法,唯一区别是从函数签名偷了一点。

#4 Expression Evaluator

calc expr
round code golf
const calc=(e)=>Math.round(eval(e));
Input 6 tokens, Output 10 tokens, Total 16 tokens

还是一样的解法,复制函数签名,但是 AC 不过;以为是优先级的问题,又把优先级的条件复制进去,还是不行;看一下发现需要取整(round),加上关键词,完事。如果强调优先级,它会生成一个很复杂的递归去解释的逻辑。

#5 Single-Source Shortest Paths

code golf

function dijkstra(
  n: number,
  edges: [u: number, v: number, w: number][],
  s: number,
): (number|null)[]
function dijkstra(n,e,s){let d=Array(n).fill(Infinity);d[s]=0;let q=new Set([s]);while(q.size){let u=[...q].reduce((a,b)=>d[a]<d[b]?a:b);q.delete(u);for(let[_,v,w]of e.filter(x=>x[0]==u)){if(d[u]+w<d[v]){d[v]=d[u]+w;q.add(v)}}}return d.map(x=>x===Infinity?null:x)}
Input 41 tokens, Output 108 tokens, Total 149 tokens

没啥特别,照样流氓解法。

#6 N-Queens

function nqueens(N: number):number
one-liner
const nqueens = N => (function solve(row, cols, diag1, diag2) { return row === N ? 1 : Array.from({ length: N }).reduce((count, _, col) => (cols[col] || diag1[row + col] || diag2[row - col + N - 1]) ? count : count + solve(row + 1, { ...cols, [col]: true }, { ...diag1, [row + col]: true }, { ...diag2, [row - col + N - 1]: true }), 0); })(0, {}, {}, {});
Input 13 tokens, Output 126 tokens, Total 139 tokens

启用了一个新的技巧 one-liner,这个词只占 2 个 token,跟 code golf 一样,效果差不多(主要是 code golf 在这里不 work)。

#7 Range Sum with Updates

function segment(
  nums: number[],
  ops: [op: string, a: number, b: number][],
): number[]

set i v updates a[i] = v.
sum l r returns inclusive sum from l to r.
code golf
function segment(nums, ops) {
  const a = [...nums];
  return ops.map(([op, x, y]) => {
    if (op === 'set') a[x] = y;
    else if (op === 'sum') return a.slice(x, y + 1).reduce((s, v) => s + v, 0);
  }).filter(v => v !== undefined);
}
Input 52 tokens, Output 84 tokens, Total 136 tokens

同样的技巧,唯一的缝补是 set 和 sum 的操作看起来不是很标准,塞进去之后就能 AC。

#8 JSON Diff

function jsondiff(
  a: unknown,
  b: unknown,
): string[]

Return a lexicographically sorted list of JSON paths where two JSON-serializable values differ. Paths use '.' for keys and '[i]' for array indices; the root is ''. always start with .

jsondiff({"x":1,"y":2}, {"x":1,"y":3}) // -> [".y"]
jsondiff({"k":[1,2]}, {"k":[1]}) // -> [".k[1]"]
function jsondiff(a, b) {
    const diffs = [];

    function compare(objA, objB, path) {
        if (typeof objA !== typeof objB) {
            diffs.push(path);
            return;
        }
        if (typeof objA === 'object' && objA !== null && objB !== null) {
            if (Array.isArray(objA) && Array.isArray(objB)) {
                const maxLength = Math.max(objA.length, objB.length);
                for (let i = 0; i < maxLength; i++) {
                    compare(objA[i], objB[i], `${path}[${i}]`);
                }
            } else {
                const keysA = Object.keys(objA);
                const keysB = Object.keys(objB);
                const allKeys = new Set([...keysA, ...keysB]);
                allKeys.forEach(key => {
                    compare(objA[key], objB[key], `${path}.${key}`);
                });
            }
        } else if (objA !== objB) {
            diffs.push(path);
        }
    }

    compare(a, b, '');
    return diffs.sort();
}
Input 108 tokens, Output 232 tokens, Total 340 tokens

答得最拉的一题,复制粘贴大法搞不定,而且总是卡那个数组下标不能 AC,需要手动改一下第二个例子,属于是 few-shot 的技巧。

#9 Sudoku Solver

function sudoku(grid: ('1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'.')[][]): ('1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9')[][]
function sudoku(grid) {
    const isValid = (row, col, num) => {
        for (let i = 0; i < 9; i++) {
            if (grid[row][i] === num || grid[i][col] === num || grid[Math.floor(row / 3) * 3 + Math.floor(i / 3)][Math.floor(col / 3) * 3 + i % 3] === num) {
                return false;
            }
        }
        return true;
    };

    const solve = () => {
        for (let row = 0; row < 9; row++) {
            for (let col = 0; col < 9; col++) {
                if (grid[row][col] === '.') {
                    for (let num = 1; num <= 9; num++) {
                        const strNum = num.toString();
                        if (isValid(row, col, strNum)) {
                            grid[row][col] = strNum;
                            if (solve()) {
                                return true;
                            }
                            grid[row][col] = '.';
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    };

    solve();
    return grid;
}
Input 63 tokens, Output 254 tokens, Total 317 tokens

这题也卡了很久,直接复制粘贴函数签名就能 AC,但是没有优化的思路。

#10 Regex Matcher

full matching code golf regex10(s,p)
function regex10(s,p){return new RegExp('^'+p+'$').test(s);}
Input 9 tokens, Output 18 tokens, Total 27 tokens

这题是我排名最前的,目前正好是第十。还是一样的套路,唯一难点是那个 full matching。

心得

先说解题的技巧。

基本盘就是复制粘贴函数签名,由于都是一些很常见的算法题,所以一般 LLM 都能一遍过;如果没过,通常是因为题干中有一些特殊的要求,这个时候回去读一遍题干,把这个特殊点找出来放进 prompt,例如 2、7、8、10 这几题。

然后就是怎么压榨输出的长度。因为打题用的 JavaScript,其实有挺多技巧/语法糖可以使用,例如箭头函数(arrow)、三元运算符(ternary)等等。这个在初期有比较大的进步,然后后来想到更加机智的一行流(one-liner),再按图索骥找到王炸 code golf

另外有个不太起眼的技巧就是,你需要探索出一种超短的实现方式,通常跟你一开始写的有天壤之比,例如 4 里使用的 eval()。你可以把题干复制出来,让别的 AI 去提供解法,然后再尝试把某些关键词放进 prompt 中引诱它生成出来。当然如果你很熟悉 JavaScript,本身就知道对应的语法糖或者算法的关键词,那必然更快能 AC。

最后,还有一点个位数的空间可以压榨。实测标点符号并不重要,函数的签名多个参数可以合并一下写,很显然的参数类型可以省略等等。这些就是已经在榜单上冲得很前想再扣出来一点的技巧了。

再说说打这个题让我对 AI 编程产生的思考。

第一还是那个「AI 决定下限,人决定上限」的理论。在 8、9 这两题我表现并不佳,因为我真的没有思路怎么解这个题,自然没办法给 AI 指出一个优化的方向。当然 AI 的能力也很强,通过一个函数签名最多试几次就能 AC,而且我估计网站背后只是用的一个本地的开源小参数模型。这说明 AI 已经能有竞争力地完成一些任务。

第二就是跟 AI 的磨合。从最开始的 GitHub Copilot 的自动补全,到 Cursor 和 Claude Code,我一直在积极使用 AI 编程,就好像跟伙伴慢慢磨合,知道它的能力能很有信心地做出什么,做什么比较悬,怎么去写 prompt 效果更好。例如我一上来就知道,复制粘贴函数签名会很关键,事实也证明这能很快地 AC 大部分题。这些东西真的很难形成书面的经验,更多时候是一种感觉。

第三是编程方式和效率的变化。就像「copilot」这个名字所说的一样,LLM 时代的编程不再是人手敲代码的「刀耕火种」,人更多地需要考虑「方向」而不是具体的实施,就算你把算法倒背如流,键盘敲冒烟,依然无法比肩 LLM 几十 token/s 的输出。另外就是,如果这个时代你还不能使用 AI 编程,那你的效率跟使用 AI 的同行完全没法比。当然这个说法有点夸张,就算你使用 AI,你的「上下文」上限仍然取决于你的脑容量,或者时间精力等等。

最后是「AI 友好的代码」。在这个题库中,多数的题都是非常基础和常见的算法,所以 AI 的准确率非常高。在工作中,更多地应该使用开源、知名而且「不那么新」(要在知识截止日期前)的库,开源库也可以使用 context7 等提供文档,尽量不要重复造轮子,更加不要再搞一大堆公司的私有库。另外最好重视文档的建设,更全更准确的上下文能让 AI 更好发挥。