feat(trainer): P5 — несколько корней, эквивалентность выражений, новые темы
- движок: gen.answers → несколько корней (_checkMultiRoot, ввод через «;», сверка мультимножеством) - kind simplify: эквивалентность выражений численным сэмплингом (_sampleEquiv, _checkEquiv), фикс. точки без Math.random - exprToLatex: знаковые коэффициенты — -5x, x²−5x+6, a−(−b)→a+b (вынос ведущего минуса, схлопывание) - темы: Упрощение (подобные, скобки) + Квадратные (Виета x²+bx+c=0, разность квадратов) → 17 генераторов, 5 тем - страница: префикс «x=»/подсказка ввода и ответ-лейбл по типу задачи - смоук движка 291/291 (T11 roots, T12 simplify, T13 latex), страница 26/26, adaptive 12/12; план P5 → DONE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -128,8 +128,16 @@
|
||||
if (n.k === 'un' || n.k === 'not') return 3;
|
||||
return 5;
|
||||
}
|
||||
function _isNeg(n) { return (n.k === 'num' && n.v < 0) || (n.k === 'un' && n.op === '-'); }
|
||||
function _negate(n) { return n.k === 'num' ? { k: 'num', v: -n.v } : n.a; }
|
||||
function _isNeg(n) {
|
||||
return (n.k === 'num' && n.v < 0) || (n.k === 'un' && n.op === '-') ||
|
||||
(n.k === 'bin' && n.op === '*' && _isNeg(n.a)); // (-5)*x — отрицательное слагаемое
|
||||
}
|
||||
function _negate(n) {
|
||||
if (n.k === 'num') return { k: 'num', v: -n.v };
|
||||
if (n.k === 'un' && n.op === '-') return n.a;
|
||||
if (n.k === 'bin' && n.op === '*') return { k: 'bin', op: '*', a: _negate(n.a), b: n.b };
|
||||
return { k: 'un', op: '-', a: n };
|
||||
}
|
||||
function _wrapL(node, minPrec) {
|
||||
var s = _latex(node);
|
||||
return _prec(node) < minPrec ? '\\left(' + s + '\\right)' : s;
|
||||
@@ -174,13 +182,15 @@
|
||||
return base + '^{' + _latex(node.b) + '}';
|
||||
}
|
||||
if (op === '*') {
|
||||
if (_isNeg(node.a)) return '-' + _latex({ k: 'bin', op: '*', a: _negate(node.a), b: node.b }); // -5*x -> «-5x»
|
||||
var sep = (node.b.k === 'num' && node.b.v >= 0) ? ' \\cdot ' : ''; // знак · между числами; иначе соседство
|
||||
return _mulOperand(node.a) + sep + _mulOperand(node.b);
|
||||
}
|
||||
if (op === '%') return _wrapL(node.a, 2) + ' \\bmod ' + _wrapL(node.b, 3);
|
||||
// + или - (схлопываем a + (-b) -> a - b)
|
||||
// + или - (схлопываем a + (-b) -> a - b и a - (-b) -> a + b)
|
||||
var right = node.b, rop = op;
|
||||
if (op === '+' && _isNeg(right)) { rop = '-'; right = _negate(right); }
|
||||
else if (op === '-' && _isNeg(right)) { rop = '+'; right = _negate(right); }
|
||||
return _wrapL(node.a, 1) + ' ' + rop + ' ' + _wrapL(right, rop === '-' ? 2 : 1);
|
||||
}
|
||||
}
|
||||
@@ -204,6 +214,25 @@
|
||||
return { ok: residual <= EPS * scale, residual: residual, lhs: L, rhs: R };
|
||||
}
|
||||
|
||||
/* ── Эквивалентность выражений численным сэмплингом ──
|
||||
Истинно, если exprA и exprB совпадают в нескольких точках по переменным vars
|
||||
(для проверки упрощения/раскрытия: 3x+5x ≡ 8x, a(x+b) ≡ ax+ab). Точки
|
||||
фиксированы → детерминированно (без Math.random). */
|
||||
var _EQUIV_PTS = [-3.7, -1.3, 0.5, 2.1, 4.9, -0.9, 3.3, 1.7];
|
||||
function _sampleEquiv(exprA, exprB, vars) {
|
||||
var ca = SE().compile(String(exprA)), cb = SE().compile(String(exprB));
|
||||
if (ca.error || cb.error) return { ok: false, reason: 'parse' };
|
||||
vars = (vars && vars.length) ? vars : ['x'];
|
||||
for (var i = 0; i < _EQUIV_PTS.length; i++) {
|
||||
var env = {};
|
||||
for (var v = 0; v < vars.length; v++) env[vars[v]] = _EQUIV_PTS[(i + v * 3) % _EQUIV_PTS.length];
|
||||
var a = ca.fn(env), b = cb.fn(env);
|
||||
var scale = Math.max(1, Math.abs(a), Math.abs(b));
|
||||
if (Math.abs(a - b) > 1e-6 * scale) return { ok: false };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/* ── Материализация одного экземпляра ──
|
||||
Возвращает problem или null, если за maxTries не удалось выполнить
|
||||
ограничения / целочисленность / самопроверку. */
|
||||
@@ -230,32 +259,44 @@
|
||||
|
||||
if (gen.require && !truthy(evalExpr(gen.require, env))) continue;
|
||||
|
||||
var answer = evalExpr(gen.answer, env);
|
||||
if (gen.integerAnswer) {
|
||||
var kind = gen.kind || 'solve';
|
||||
|
||||
// корни: одиночный (answer) или множественный (answers — массив выражений)
|
||||
var answers = null;
|
||||
if (Array.isArray(gen.answers)) {
|
||||
answers = gen.answers.map(function (a) { return evalExpr(a, env); });
|
||||
if (gen.integerAnswer) answers = answers.map(function (x) { return Math.round(x); });
|
||||
}
|
||||
var answer = gen.answer ? evalExpr(gen.answer, env) : (answers ? answers[0] : 0);
|
||||
if (gen.answer && gen.integerAnswer) {
|
||||
if (!isIntApprox(answer)) continue;
|
||||
answer = Math.round(answer);
|
||||
}
|
||||
|
||||
var lhsExpr = render(gen.lhs, env);
|
||||
var rhsExpr = render(gen.rhs, env);
|
||||
var lhsExpr = render(gen.lhs || 'x', env);
|
||||
var rhsExpr = render(gen.rhs || 'x', env);
|
||||
var sEnv = assign(env, { ans: answer });
|
||||
// compute-задача (проценты): показываем текстовый prompt из display, а
|
||||
// уравнение lhs=rhs служит лишь для проверки → latex уравнения не строим.
|
||||
var isCompute = gen.kind === 'compute';
|
||||
var ll = isCompute ? null : exprToLatex(lhsExpr);
|
||||
var rl = isCompute ? null : exprToLatex(rhsExpr);
|
||||
// latex уравнения строим только для уравнений (solve/roots); compute/simplify —
|
||||
// текстовый prompt из display.
|
||||
var showEq = (kind === 'solve' || kind === 'roots');
|
||||
var ll = showEq ? exprToLatex(lhsExpr) : null;
|
||||
var rl = showEq ? exprToLatex(rhsExpr) : null;
|
||||
var answerExpr = gen.answerExpr ? render(gen.answerExpr, env) : null;
|
||||
|
||||
var problem = {
|
||||
genId: gen.id,
|
||||
skill: gen.skill || gen.id, // ключ прогресса = id генератора, если skill не задан
|
||||
title: gen.title,
|
||||
kind: gen.kind || 'solve',
|
||||
kind: kind,
|
||||
lhsExpr: lhsExpr,
|
||||
rhsExpr: rhsExpr,
|
||||
display: prettyMath(render(gen.display || (gen.lhs + ' = ' + gen.rhs), env)),
|
||||
latex: (ll != null && rl != null) ? (ll + ' = ' + rl) : null,
|
||||
answerVar: answerVar,
|
||||
answer: answer,
|
||||
answers: answers, // массив корней (kind roots)
|
||||
answerExpr: answerExpr, // канон. выражение (kind simplify)
|
||||
answerVars: gen.answerVars || [answerVar],
|
||||
params: env,
|
||||
// шаг решения -> { note(текст), tex(подпись), latex(для KaTeX, null если не разобрался) }
|
||||
// строковый шаг (легаси) трактуется как чистая заметка без формулы.
|
||||
@@ -270,13 +311,20 @@
|
||||
})
|
||||
};
|
||||
|
||||
// Самопроверка: эталонный корень ОБЯЗАН удовлетворять уравнению.
|
||||
var v = verifyRoot(problem, answer);
|
||||
if (!v.ok) {
|
||||
if (opts.strict) {
|
||||
throw new Error('Генератор «' + gen.id + '»: корень ' + fmtNum(answer) +
|
||||
' не удовлетворяет уравнению (невязка ' + v.residual + ').');
|
||||
}
|
||||
// Самопроверка по типу: simplify → эквивалентность; roots → все корни; иначе → корень.
|
||||
var okSelf, why;
|
||||
if (kind === 'simplify') {
|
||||
okSelf = _sampleEquiv(render(gen.srcExpr || gen.lhs || 'x', env), answerExpr, problem.answerVars).ok;
|
||||
why = 'упрощение не эквивалентно ответу';
|
||||
} else if (answers) {
|
||||
okSelf = answers.every(function (r) { return verifyRoot(problem, r).ok; });
|
||||
why = 'не все корни удовлетворяют уравнению';
|
||||
} else {
|
||||
var v = verifyRoot(problem, answer);
|
||||
okSelf = v.ok; why = 'корень ' + fmtNum(answer) + ' не удовлетворяет (невязка ' + v.residual + ')';
|
||||
}
|
||||
if (!okSelf) {
|
||||
if (opts.strict) throw new Error('Генератор «' + gen.id + '»: ' + why + '.');
|
||||
continue;
|
||||
}
|
||||
return problem;
|
||||
@@ -309,6 +357,9 @@
|
||||
var raw = String(input == null ? '' : input).trim();
|
||||
if (!raw) return { ok: false, reason: 'empty', value: null, residual: null, message: 'Введите ответ.' };
|
||||
|
||||
if (problem.kind === 'simplify') return _checkEquiv(problem, raw);
|
||||
if (problem.kind === 'roots') return _checkMultiRoot(problem, raw);
|
||||
|
||||
var c = SE().compile(raw);
|
||||
if (c.error) {
|
||||
return { ok: false, reason: 'parse', value: null, residual: null,
|
||||
@@ -328,6 +379,40 @@
|
||||
};
|
||||
}
|
||||
|
||||
/* Несколько корней: ученик вводит все через «;»/«,»/пробел; сверяем как мультимножество. */
|
||||
function _checkMultiRoot(problem, raw) {
|
||||
var parts = raw.split(/[;,\s]+/).filter(Boolean);
|
||||
if (!parts.length) return { ok: false, reason: 'empty', message: 'Введите ответ.' };
|
||||
var vals = [];
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var c = SE().compile(parts[i]);
|
||||
if (c.error) return { ok: false, reason: 'parse', message: 'Не понял ответ.' };
|
||||
var x = c.fn({});
|
||||
if (!isFinite(x)) return { ok: false, reason: 'nan', message: 'Это не число.' };
|
||||
vals.push(x);
|
||||
}
|
||||
var want = (problem.answers || []).slice();
|
||||
if (vals.length !== want.length) return { ok: false, reason: 'count', message: 'Укажите все корни через «;».' };
|
||||
var used = want.map(function () { return false; });
|
||||
for (var j = 0; j < vals.length; j++) {
|
||||
var f = -1;
|
||||
for (var w = 0; w < want.length; w++) {
|
||||
if (!used[w] && Math.abs(vals[j] - want[w]) <= 1e-6 * Math.max(1, Math.abs(want[w]))) { f = w; break; }
|
||||
}
|
||||
if (f < 0) return { ok: false, reason: 'wrong', message: 'Пока неверно.' };
|
||||
used[f] = true;
|
||||
}
|
||||
return { ok: true, reason: null, message: 'Верно!' };
|
||||
}
|
||||
|
||||
/* Упрощение: ответ-выражение проверяем на эквивалентность сэмплингом. */
|
||||
function _checkEquiv(problem, raw) {
|
||||
var c = SE().compile(raw);
|
||||
if (c.error) return { ok: false, reason: 'parse', message: 'Не понял выражение: ' + c.error };
|
||||
var se = _sampleEquiv(raw, problem.answerExpr, problem.answerVars || ['x']);
|
||||
return { ok: se.ok, reason: se.ok ? null : (se.reason || 'wrong'), value: raw, message: se.ok ? 'Верно!' : 'Пока неверно.' };
|
||||
}
|
||||
|
||||
global.TrainerEngine = {
|
||||
instantiate: instantiate,
|
||||
generateBatch: generateBatch,
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
var TOPICS = [
|
||||
{ key: 'linear-eq', label: 'Уравнения', subject: 'algebra', grade: 7, order: 1 },
|
||||
{ key: 'proportions', label: 'Пропорции', subject: 'algebra', grade: 7, order: 2 },
|
||||
{ key: 'percents', label: 'Проценты', subject: 'algebra', grade: 7, order: 3 }
|
||||
{ key: 'percents', label: 'Проценты', subject: 'algebra', grade: 7, order: 3 },
|
||||
{ key: 'simplify', label: 'Упрощение', subject: 'algebra', grade: 7, order: 4 },
|
||||
{ key: 'quadratic', label: 'Квадратные', subject: 'algebra', grade: 8, order: 5 }
|
||||
];
|
||||
|
||||
var GENERATORS = [
|
||||
@@ -249,6 +251,67 @@
|
||||
{ note: 'Известно, что {p}% некоторого числа равны {a}. Значит само число во столько раз больше: умножаем {a} на 100 и делим на {p}.', tex: 'x = {a}*100/{p}' },
|
||||
{ note: 'Считаем — получаем искомое число.', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* ═══ Тема: Упрощение выражений (проверка эквивалентностью) ═══ */
|
||||
|
||||
/* a·x + b·x → (a+b)x */
|
||||
{
|
||||
id: 'simp-like', topic: 'simplify', order: 1, subject: 'algebra', grade: 7, kind: 'simplify',
|
||||
title: 'Привести подобные',
|
||||
pick: { a: [2, 9], b: [2, 9] },
|
||||
derive: { s: 'a + b' },
|
||||
srcExpr: '{a}*x + {b}*x', answerExpr: '{s}*x', answerVars: ['x'],
|
||||
display: 'Упростите: {a}x + {b}x',
|
||||
solution: [
|
||||
{ note: 'Оба слагаемых содержат x — это подобные слагаемые. Складываем их коэффициенты: {a} + {b} = {s}.', tex: '{a}x + {b}x = {s}x' }
|
||||
]
|
||||
},
|
||||
|
||||
/* a(x + b) → ax + ab */
|
||||
{
|
||||
id: 'simp-expand', topic: 'simplify', order: 2, subject: 'algebra', grade: 7, kind: 'simplify',
|
||||
title: 'Раскрыть скобки',
|
||||
pick: { a: [2, 9], b: [1, 9] },
|
||||
derive: { ab: 'a*b' },
|
||||
srcExpr: '{a}*(x + {b})', answerExpr: '{a}*x + {ab}', answerVars: ['x'],
|
||||
display: 'Раскройте скобки: {a}(x + {b})',
|
||||
solution: [
|
||||
{ note: 'Умножаем множитель {a} на каждое слагаемое внутри скобки.', tex: '{a}(x + {b}) = {a}x + {ab}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* ═══ Тема: Квадратные уравнения (несколько корней) ═══ */
|
||||
|
||||
/* x² + bx + c = 0 — разложение по Виета (два корня r1, r2) */
|
||||
{
|
||||
id: 'quad-factored', topic: 'quadratic', order: 1, subject: 'algebra', grade: 8, kind: 'roots',
|
||||
title: 'x² + bx + c = 0',
|
||||
pick: { r1: [-7, 7], r2: [-7, 7] },
|
||||
constraint: 'r1 != r2',
|
||||
derive: { b: '-(r1 + r2)', c: 'r1*r2' },
|
||||
lhs: 'x^2 + {b}*x + {c}', rhs: '0',
|
||||
answerVar: 'x', answers: ['r1', 'r2'], integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Квадратное уравнение приравнено к нулю. По теореме Виета ищем два числа: их сумма равна {r1}+{r2}, произведение — {c}. Это и есть корни. Раскладываем на множители:', tex: '(x - {r1})(x - {r2}) = 0' },
|
||||
{ note: 'Произведение равно нулю, когда обнуляется множитель. Первый корень:', tex: 'x = {r1}' },
|
||||
{ note: 'Второй корень:', tex: 'x = {r2}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* x² − a² = 0 — разность квадратов (корни ±a) */
|
||||
{
|
||||
id: 'quad-diff', topic: 'quadratic', order: 2, subject: 'algebra', grade: 8, kind: 'roots',
|
||||
title: 'x² − a² = 0',
|
||||
pick: { a: [2, 9] },
|
||||
derive: { a2: 'a*a' },
|
||||
lhs: 'x^2 - {a2}', rhs: '0',
|
||||
answerVar: 'x', answers: ['a', '-a'], integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Слева — разность квадратов: x² − {a2} = (x − {a})(x + {a}). Раскладываем:', tex: '(x - {a})(x + {a}) = 0' },
|
||||
{ note: 'Первый корень:', tex: 'x = {a}' },
|
||||
{ note: 'Второй корень:', tex: 'x = -{a}' }
|
||||
]
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
+23
-5
@@ -141,7 +141,7 @@
|
||||
<main class="sb-content">
|
||||
<div class="tr-wrap">
|
||||
<div class="tr-head">
|
||||
<h1 class="tr-h1">Тренажёр<span class="tr-pill" id="tr-subject">Алгебра · 7 класс</span></h1>
|
||||
<h1 class="tr-h1">Тренажёр<span class="tr-pill" id="tr-subject">Алгебра · 7–8 класс</span></h1>
|
||||
<div class="tr-sub">Задачи генерируются автоматически и проверяются мгновенно. Решай по одной — бесконечно.</div>
|
||||
</div>
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<div class="tr-eq" id="tr-eq">—</div>
|
||||
|
||||
<div class="tr-inrow">
|
||||
<span class="tr-eqx">x =</span>
|
||||
<span class="tr-eqx" id="tr-eqx">x =</span>
|
||||
<input class="tr-input" id="tr-input" type="text" inputmode="text" autocomplete="off"
|
||||
placeholder="ответ" aria-label="Ваш ответ"/>
|
||||
<button class="tr-btn tr-primary" id="tr-check" type="button">Проверить</button>
|
||||
@@ -299,6 +299,7 @@
|
||||
cur = wordPool[wordIdx % wordPool.length]; wordIdx++;
|
||||
$('tr-skill').textContent = cur.title;
|
||||
setMath(eq, null, cur.display, true); // условие как текст
|
||||
applyInputMode();
|
||||
var inp = $('tr-input'); inp.value = ''; inp.disabled = false;
|
||||
setMode(false); inp.focus();
|
||||
}
|
||||
@@ -362,6 +363,19 @@
|
||||
answered = done;
|
||||
$('tr-check').textContent = done ? 'Дальше' : 'Проверить';
|
||||
}
|
||||
// Префикс «x =» и подсказка ввода зависят от типа задачи.
|
||||
function applyInputMode() {
|
||||
var k = cur && cur.kind;
|
||||
var multi = (k === 'roots' || k === 'simplify');
|
||||
var eqx = $('tr-eqx'); if (eqx) eqx.style.display = multi ? 'none' : '';
|
||||
$('tr-input').placeholder = (k === 'roots') ? 'корни через ;' : (k === 'simplify') ? 'упрощённое выражение' : 'ответ';
|
||||
}
|
||||
// Текст ответа в фидбеке/раскрытии — по типу задачи.
|
||||
function answerLabel() {
|
||||
if (cur.kind === 'roots' && cur.answers) return 'Корни: ' + cur.answers.map(fmt).join('; ');
|
||||
if (cur.kind === 'simplify') return '= ' + (cur.answerExpr ? fmt(cur.answerExpr) : '');
|
||||
return 'x = ' + fmt(cur.answer);
|
||||
}
|
||||
function updateStats() { $('tr-solved').textContent = solved; $('tr-streak').textContent = streak; }
|
||||
|
||||
function stepHtml(st, n) {
|
||||
@@ -384,8 +398,9 @@
|
||||
|
||||
$('tr-skill').textContent = curGen.title;
|
||||
var eq = $('tr-eq');
|
||||
eq.classList.toggle('tr-eq-text', !cur.latex); // текстовый prompt (проценты) — другим шрифтом
|
||||
eq.classList.toggle('tr-eq-text', !cur.latex); // текстовый prompt (проценты/упрощение) — другим шрифтом
|
||||
setMath(eq, cur.latex, cur.display, true);
|
||||
applyInputMode();
|
||||
var inp = $('tr-input');
|
||||
inp.value = ''; inp.disabled = false;
|
||||
var fb = $('tr-feedback'); fb.className = 'tr-feedback'; fb.textContent = '';
|
||||
@@ -467,7 +482,8 @@
|
||||
streak = 0;
|
||||
$('tr-input').disabled = true;
|
||||
var fb = $('tr-feedback'); fb.className = 'tr-feedback';
|
||||
setMath(fb, 'x = ' + cur.answer, 'Ответ: x = ' + fmt(cur.answer), false);
|
||||
if (cur.kind === 'roots' || cur.kind === 'simplify') fb.textContent = 'Ответ: ' + answerLabel();
|
||||
else setMath(fb, 'x = ' + cur.answer, 'Ответ: x = ' + fmt(cur.answer), false);
|
||||
setMode(true);
|
||||
recordAnswer(false); submitAttempt(false);
|
||||
updateStats();
|
||||
@@ -486,7 +502,9 @@
|
||||
if (r.ok) {
|
||||
solved++; streak++;
|
||||
fb.className = 'tr-feedback ok';
|
||||
fb.innerHTML = ICON.ok + ' <span>Верно!</span> ' + (kat('x = ' + cur.answer, false) || esc('x = ' + fmt(cur.answer)));
|
||||
var lbl = (cur.kind === 'roots' || cur.kind === 'simplify') ? esc(answerLabel())
|
||||
: (kat('x = ' + cur.answer, false) || esc('x = ' + fmt(cur.answer)));
|
||||
fb.innerHTML = ICON.ok + ' <span>Верно!</span> ' + lbl;
|
||||
recordAnswer(true); submitAttempt(true);
|
||||
} else {
|
||||
streak = 0;
|
||||
|
||||
@@ -108,9 +108,19 @@ practice.test.js 11/11 (+SR box/due).
|
||||
- **Acceptance:** учитель собирает рабочий генератор без кода; ученик решает; права/видимость
|
||||
как у custom-sim (own + раздано).
|
||||
|
||||
## Phase 5 — Типы ответов и проверки
|
||||
## Phase 5 — Типы ответов и проверки — DONE (частично)
|
||||
|
||||
**Цель:** не только «корень-число».
|
||||
**Сделано:** движок получил **несколько корней** (`gen.answers` → `problem.answers`;
|
||||
`_checkMultiRoot` — ввод всех корней через «;», сверка мультимножеством) и
|
||||
**эквивалентность выражений** (`kind:'simplify'`, `gen.srcExpr`/`answerExpr`;
|
||||
`_sampleEquiv` — численный сэмплинг в фикс. точках, без Math.random; `_checkEquiv`).
|
||||
`exprToLatex` чинит знаковые коэффициенты (`-5x`, `x²−5x+6`, `a−(−b)→a+b`). Новые
|
||||
темы: **Упрощение** (привести подобные, раскрыть скобки) и **Квадратные** (Виета
|
||||
`x²+bx+c=0`, разность квадратов — 2 корня). Страница: префикс «x=» и подсказка ввода
|
||||
по типу, ответ-лейбл (корни/выражение). Смоук движка 291/291 (T11 roots, T12 simplify,
|
||||
T13 latex). **Осталось (стретч):** неравенства (нужен парсер отношений) — не вошло.
|
||||
|
||||
**Цель (исходная):** не только «корень-число».
|
||||
|
||||
- Множество корней (квадратные/факторизация), интервалы (неравенства), упрощение выражений
|
||||
(эквивалентность через численный сэмплинг по диапазону, а не строковое равенство).
|
||||
|
||||
Reference in New Issue
Block a user