私はKattis problem に取り組んでいます。ここでは、入力をプレフィックス表記で受け取り、それを簡略化してプレフィックス表記で返すことになっています。これらは入力と出力の例です。
Sample Input 1 Sample Output 1
+ 3 4 Case 1: 7
- x x Case 2: - x x
* - 6 + x -6 - - 9 6 * 0 c Case 3: * - 6 + x -6 - 3 * 0 c
このコードを記述しました。この種の入力データで実行すると、上記とまったく同じ出力が得られます。それでも、私はカティスから間違った答えを得ます。
ここで私が間違っているのは何ですか?デバッグのヒントが得られないのでイライラします。
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const operators = ['+', '-', '*', '/'];
const operatorsFunctions = {
'+': (a, b) => a + b,
'-': (a, b) => a - b,
'*': (a, b) => a * b,
'/': (a, b) => a / b,
};
let lineNumber = 0;
rl.on('line', (line) => {
const mathExpression = line.split(' ');
lineNumber += 1;
let result = [];
let stack = [];
for (let i = mathExpression.length -1; i >= 0; i--) {
if (!isNaN(mathExpression[i])) {
stack.unshift(mathExpression[i]);
} else if (operators.includes(mathExpression[i])){
if (!stack.length) {
result.unshift(mathExpression[i]);
}
if (stack.length === 1) {
result.unshift(stack[0]);
result.unshift(mathExpression[i]);
stack = [];
}
if (stack.length > 1) {
const sum = operatorsFunctions[mathExpression[i]](Number(stack[0]), Number(stack[1]))
stack.splice(0, 2, sum);
if (i === 0) {
result.unshift(...stack);
}
}
} else {
if (stack.length) {
result.unshift(...stack);
stack = [];
}
result.unshift(mathExpression[i]);
}
}
const text = `Case ${lineNumber}: ${result.join(' ')}`;
console.log(text);
});
この問題を解決する手順は簡単です。
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const ops = ["+", "-", "/", "*"];
let lineNumber = 0;
rl.on('line', (line) => {
lineNumber += 1;
let exp = line.split(" ");
for (let i = exp.length - 2; i >= 0 ; i--) {
if (ops.includes(exp[i])) {
if (![exp[i+1], exp[i+2]].map(Number).some(Number.isNaN)) {
exp.splice(i, 3, eval([exp[i+1], exp[i], exp[i+2]].join(" ")));
} else { // a letter detected - we can safely skip two items
i -= 2;
}
}
}
console.log(`Case ${lineNumber}: ${exp.join(" ")}`);
});
そして、誰かがレデューサーと高次関数、不変性*、参照テストの透過性*を備えた、長くてよく説明された関数型コードを気に入る場合、これは次のとおりです。
const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
let lineNumber = 0;
rl.on("line", line => {
lineNumber += 1;
let tokens = line.split(" ");
let simplified = tokens.reduceRight(simplify(), []);
console.log(`Case ${lineNumber}: ${simplified.join(" ")}`);
});
function simplify() {
const operations = {
"+": (a, b) => a + b,
"-": (a, b) => a - b,
"*": (a, b) => a * b,
"/": (a, b) => a / b
};
const skip = { val: 2 };
const doWork = createDoWork(skip, operations);
return (simplified, token) => {
if (skip.val) {
skip.val--;
return [token, ...simplified];
}
return doWork(simplified, token);
};
}
function createDoWork(skip, operations) {
const isOperator = createIsOperator(operations);
const replaceWithEvaluation = createReplaceWithEvaluation(operations);
return (simplified, token) => {
if (isOperator(token)) {
if (firstTwoAreNumbers(simplified)) {
return replaceWithEvaluation(token, simplified);
}
skip.val = 2;
}
return [token, ...simplified];
};
}
function createIsOperator(operations) {
const operationTokens = Object.keys(operations);
return token => operationTokens.includes(token);
}
function firstTwoAreNumbers(arr) {
return !arr
.slice(0, 2)
.map(Number)
.some(Number.isNaN);
}
function createReplaceWithEvaluation(operations) {
return (operator, simplified) => {
const [n1, n2, ...rest] = simplified;
const evaluation = operations[operator](+n1, +n2);
return [evaluation, ...rest];
};
}
*コードを最大3倍高速化する小さな最適化がありますが、コードの一部が不純になります。私はそれをリファクタリングする仕事を好奇心の強い読者に任せます;)
これはKattisテストスイートに合格しない可能性が高いですが、別のアプローチを共有したかっただけです
まず、式をデータ構造に変換します。
tokenize('+ x + 10 20');
//=> ['+', 'x', ['+', '10', '20']]
どうして? "O A B"式を再帰的に解釈できます:
const simplify_expr = ([o, a, b]) =>
interpret(
[ o
, is_expr(a) ? simplify_expr(a) : evaluate(a)
, is_expr(b) ? simplify_expr(b) : evaluate(b)
]);
simplify_expr(['+', 'x', ['+', '10', '20']]);
//=> ['+', 'x', 30]
次の簡略化手順を考えます。
簡略化手順は、変数を含まない部分式を可能な限りその値で置き換えるだけです。
次に、interpret
関数は次のように記述できます。
const interpret = ([o, a, b]) =>
typeof a !== 'number' || typeof b !== 'number' ? [o, a, b]
: o === '*' ? a * b
: o === '/' ? a / b
: o === '+' ? a + b
: a - b;
interpret(['+', 10, 20]);
//=> 30
文字列を分割します。
'+ x + 10 + 20 30'.split(' ')
//=> ['+', 'x', '+', '10', '+', '20', '30']
次に、すべての式を3つのグループでグループ化するまで、右から左に再帰します。
['+', 'x', '+', '10', '+', '20', '30'] // length > 3
['+', 'x', '+', '10', ['+', '20', '30']] // length > 3
['+', 'x', ['+', '10', ['+', '20', '30']]] // length 3 stop!
可能な実装:
const group_expr = xs =>
xs.length <= 3
? xs
: is_expr(xs.slice(-3))
? group_expr(
[ ...xs.slice(0, -3)
, xs.slice(-3)
])
: group_expr(
[ ...xs.slice(0, -4)
, xs.slice(-4, -1)
, ...xs.slice(-1)
]);
const tokenize = str =>
group_expr(str.split(' '));
⚠️これはArray.prototype.flat
Edgeではサポートされていません。
const evaluate = x =>
Number(x) == x
? Number(x)
: x;
const is_expr = x =>
Array.isArray(x) &&
( x[0] === '*' ||
x[0] === '/' ||
x[0] === '+' ||
x[0] === '-' );
const group_expr = xs =>
xs.length <= 3
? xs
: is_expr(xs.slice(-3))
? group_expr(
[ ...xs.slice(0, -3)
, xs.slice(-3)
])
: group_expr(
[ ...xs.slice(0, -4)
, xs.slice(-4, -1)
, ...xs.slice(-1)
]);
const tokenize = str =>
group_expr(str.split(' '));
const interpret = ([o, a, b]) =>
typeof a !== 'number' || typeof b !== 'number' ? [o, a, b]
: o === '*' ? a * b
: o === '/' ? a / b
: o === '+' ? a + b
: a - b;
const simplify_expr = ([o, a, b]) =>
interpret(
[ o
, is_expr(a) ? simplify_expr(a) : evaluate(a)
, is_expr(b) ? simplify_expr(b) : evaluate(b)
]);
const simplify = str => {
const expr = simplify_expr(tokenize(str));
return Array.isArray(expr)
? expr.flat(Infinity).join(' ')
: String(expr);
};
console.log(simplify('+ 3 4'));
console.log(simplify('- x x'));
console.log(simplify('* - 6 + x -6 - - 9 6 * 0 c'));
console.log(simplify('+ x + 10 + 20 30'));