我的同学出了一道很有趣的题目,一起来看看:
假设有一个数列 ${a_n}$:
$$
a_n =
\begin{cases}
1, & \text{当 } n \equiv 1 \pmod{3} \
4, & \text{当 } n \equiv 2 \pmod{3} \
4, & \text{当 } n \equiv 0 \pmod{3}
\end{cases}
$$ 其长度为 $3k$,其中每个元素按顺序排在一起.你需要通过加,减,乘,除四则运算将这些元素连接在一起,并且可以使用括号调整运算优先级,最终得到结果为 $1635$.顺序必须保持原数列中的顺序,即不能重新排列元素.求出最小的 $k$ ,使得通过这些操作能够得到结果 $1635$.
计算机真是快呢! 不到 1 秒就可以出结果了.
我们现在来思考这个问题.
这道题目有些像小时候做过的"24 点问题", 通过加减乘除凑出一个表达式,使其结果为 $24$ .
但不同的是这道题目里数更多, 并且支持括号. 这道题本质上就是给定一个特定的数组,将其分为左,右两半部分,再用加减乘除之一把它们合起来.
因此表达式可以为:
左区间的表达式 op 右区间的表达式 (op为运算符)
很自然地, 我们想到 区间 DP.
定义 $dp[l][r]$ 表示用第 $l$ 个到第 $r$ 个数,保持原顺序,任意加括号,插入四则运算以后,所有能得到的结果.
由于一个区间可能得到很多不同结果,所以 $dp[l][r]$不是单个数,而是一个哈希表.这个哈希表的键为一个分数, 其值为得到这个分数的表达式的string
只看前三个数 $1,4,4$.
长度为 1 时:
$$
\begin{aligned}
dp[1][1] &= {1} \
dp[2][2] &= {4} \
dp[3][3] &= {4}
\end{aligned}
$$
长度为 2 时:
$dp[1][2]$ 由 $1$ 和 $4$ 合并得到:
$$ \begin{aligned} 1+4 &= 5 \ 1-4 &= -3 \ 1\times4 &= 4 \ 1\div4 &= \frac{1}{4} \end{aligned} $$
所以 $dp[1][2]={5,-3,4,\frac{1}{4}}$
同理 $dp[2][3]$ 由 $4$ 和 $4$ 得到:
${8,0,16,1}$
长度为 3 时,要算 $dp[1][3]$.
枚举断点:
第一种切法是 $dp[1][1]$ 和 $dp[2][3]$
也就是拿 $dp[1][1]$ 里的$1$,和 $dp[2][3]$ 里的 ${8,0,16,1}$ 逐个配对做四则运算
第二种切法是 $dp[1][2]$ 和 $dp[3][3]$
也就是拿 $dp[1][2]$ 里的 ${5,-3,4,\frac{1}{4}}$,和 $dp[3][3]$ 里的 $4$去配对
之后,再类似地推到 $dp[1][3k]$, 这样我们就可以知道在 $r=3k$ 下能算出多少种数, 再从中查找$1635$, 若没有,则使$k++$并再算一次, 若有则输出结果为$1635$的表达式.因此,想要解决这个问题,自小到大地枚举 $k \in \mathbb{N}^+$ , 使得 $k$ 能满足题目条件, 即$dp[1][3k]$中有一个表达式的值为$1635$.
(代码中的变量名不全都和上文中的对应,见谅)
#include <iostream>
#include <unordered_map>
#include <string>
#include <cstdlib>
#define ll long long
#define N 44 // shenmizhe
#define TARGET 1635
using namespace std;
ll gcdll(ll a, ll b) {
while (b) {
ll t = a % b;
a = b;
b = t;
}
return a;
}
struct Frac {
ll p, q;
Frac(ll _p = 0, ll _q = 1) {
if (_q < 0) _p = -_p, _q = -_q;
if (_p == 0) {
p = 0;
q = 1;
} else {
ll g = gcdll(llabs(_p), llabs(_q));
p = _p / g;
q = _q / g;
}
}
bool operator == (const Frac& b) const {
return p == b.p && q == b.q;
}
};
struct HashFrac {
size_t operator () (const Frac& x) const {
return (size_t)(x.p * 1000003LL + x.q);
}
};
Frac addf(Frac a, Frac b) {
return Frac(a.p * b.q + b.p * a.q, a.q * b.q);
}
Frac subf(Frac a, Frac b) {
return Frac(a.p * b.q - b.p * a.q, a.q * b.q);
}
Frac mulf(Frac a, Frac b) {
return Frac(a.p * b.p, a.q * b.q);
}
bool divf(Frac a, Frac b, Frac& c) { // 分母不得为 0, 否则会发生除以 0 的错误
if (b.p == 0) return false;
c = Frac(a.p * b.q, a.q * b.p);
return true;
}
int a[N], m;
Frac tgt(TARGET, 1); // 目标分数
unordered_map<Frac, string, HashFrac> dp[N][N];
void ins(unordered_map<Frac, string, HashFrac>& mp, Frac v, string s) {
if (!mp.count(v)) mp[v] = s; // 不在哈希表内就加入哈希表
}
bool solve(int n) {
m = 3 * n;
for (int i = 1; i <= n; i++) { // init
a[i * 3 - 2] = 1;
a[i * 3 - 1] = 4;
a[i * 3] = 4;
}
for (int i = 1; i <= m; i++) {
for (int j = i; j <= m; j++) {
dp[i][j].clear();
}
}
for (int i = 1; i <= m; i++) {
dp[i][i][Frac(a[i], 1)] = to_string(a[i]);
}
for (int len = 2; len <= m; len++) {
for (int l = 1; l + len - 1 <= m; l++) {
int r = l + len - 1;
for (int k = l; k < r; k++) {
// c++11
for (auto &L : dp[l][k]) {
for (auto &R : dp[k + 1][r]) {
ins(dp[l][r], addf(L.first, R.first), "(" + L.second + "+" + R.second + ")");
ins(dp[l][r], subf(L.first, R.first), "(" + L.second + "-" + R.second + ")");
ins(dp[l][r], mulf(L.first, R.first), "(" + L.second + "*" + R.second + ")");
Frac v;
if (divf(L.first, R.first, v)) {
ins(dp[l][r], v, "(" + L.second + "/" + R.second + ")");
}
}
}
}
}
}
if (dp[1][m].count(tgt)) {
printf("Found min n=%d\n Expr %s",n,dp[1][m][tgt].c_str()); // 不喜欢用 cout
return true;
}
return false;
}
int main() {
for (int n = 1; n <= 8; n++) {
if (solve(n)) return 0;
}
printf("No solution\n");
return 0;
}
但是经过测试发现这段代码运行得较慢
原来 对于每个区间$[l,r]$,都把这个区间所有可能结果完整求出来,这是全状态区间 DP,
于是我们可以对其进行修改, 对较小的区间进行预处理,而对于大区间我们采用定向 dfs,这样可以节约 999 时间.
此外还有一些修改,比如使用反推,以及string all_add(int l, int r),在代码中已经做了解释,相信读者能够看懂.
#include <iostream>
#include <unordered_map>
#include <string>
#include <cstdlib>
#define ll long long
#define N 44 // shenmizhe
#define TARGET 1635
using namespace std;
ll gcdll(ll a, ll b) {
while (b) {
ll t = a % b;
a = b;
b = t;
}
return a;
}
struct Frac {
ll p, q;
Frac(ll _p = 0, ll _q = 1) {
if (_q < 0) _p = -_p, _q = -_q;
if (_p == 0) {
p = 0;
q = 1;
} else {
ll g = gcdll(llabs(_p), llabs(_q));
p = _p / g;
q = _q / g;
}
}
bool operator == (const Frac& b) const {
return p == b.p && q == b.q;
}
};
struct HashFrac {
size_t operator () (const Frac& x) const {
return (size_t)(x.p * 1000003LL + x.q);
}
};
Frac addf(Frac a, Frac b) {
return Frac(a.p * b.q + b.p * a.q, a.q * b.q);
}
Frac subf(Frac a, Frac b) {
return Frac(a.p * b.q - b.p * a.q, a.q * b.q);
}
Frac mulf(Frac a, Frac b) {
return Frac(a.p * b.p, a.q * b.q);
}
bool divf(Frac a, Frac b, Frac& c) { // 分母不得为 0, 否则会发生除以 0 的错误
if (b.p == 0) return false;
c = Frac(a.p * b.q, a.q * b.p);
return true;
}
int a[N], m, K; // 只完整预处理长度 <= K 的区间
Frac tgt(TARGET, 1); // 目标分数
unordered_map<Frac, string, HashFrac> dp[N][N];
void ins(unordered_map<Frac, string, HashFrac>& mp, Frac v, string s) {
if (!mp.count(v)) mp[v] = s; // 不在哈希表内就加入哈希表
}
struct Jiyi {
int l, r;
Frac t;
bool operator == (const Jiyi& b) const {
return l == b.l && r == b.r && t == b.t;
}
};
struct HashJiyi {
size_t operator () (const Jiyi& x) const {
size_t h = (size_t)x.l;
h = h * 1000003ULL + (size_t)x.r;
h = h * 1000003ULL + (size_t)(x.t.p * 1000003LL + x.t.q);
return h;
}
};
unordered_map<Jiyi, string, HashJiyi> jiyis;
/*
在某些特殊情况下,目标反推不是唯一的.
0 * y = 0
这时候 y 可以是很多东西,没法从 0 唯一反推出右边目标.
因为题目里的数都是正数 1 和 4,就随便把剩下那段拼成一个合法表达式,例如一路加起来:
(((1+4)+4)+1)...
该表达式一定合法.
*/
string all_add(int l, int r) {
string s = to_string(a[l]);
for (int i = l + 1; i <= r; i++) {
s = "(" + s + "+" + to_string(a[i]) + ")";
}
return s;
}
string dfs(int l, int r, Frac T) {
Jiyi now = {l, r, T};
if (jiyis.count(now)) return jiyis[now];
int len = r - l + 1;
if (len <= K) {
if (dp[l][r].count(T)) return jiyis[now] = dp[l][r][T];
return jiyis[now] = "";
}
for (int k = l; k < r; k++) {
int lenL = k - l + 1;
int lenR = r - k;
// 总是枚举较短的那一边,减少状态数
if (lenL <= lenR) {
if (lenL > K) continue;
for (auto &L : dp[l][k]) {
Frac x = L.first, need;
string ex = L.second, ey;
// x + y = T biancheng y = T - x
need = subf(T, x);
ey = dfs(k + 1, r, need);
if (!ey.empty()) return jiyis[now] = "(" + ex + "+" + ey + ")";
need = subf(x, T);
ey = dfs(k + 1, r, need);
if (!ey.empty()) return jiyis[now] = "(" + ex + "-" + ey + ")";
if (x.p != 0) {
if (divf(T, x, need)) {
ey = dfs(k + 1, r, need);
if (!ey.empty()) return jiyis[now] = "(" + ex + "*" + ey + ")";
}
} else if (T.p == 0) {
return jiyis[now] = "(" + ex + "*" + all_add(k + 1, r) + ")";
}
if (T.p != 0) {
if (divf(x, T, need) && need.p != 0) {
ey = dfs(k + 1, r, need);
if (!ey.empty()) return jiyis[now] = "(" + ex + "/" + ey + ")";
}
} else if (x.p == 0) {
return jiyis[now] = "(" + ex + "/" + all_add(k + 1, r) + ")";
}
}
} else {
if (lenR > K) continue;
for (auto &R : dp[k + 1][r]) {
Frac y = R.first, need;
string ey = R.second, ex;
need = subf(T, y);
ex = dfs(l, k, need);
if (!ex.empty()) return jiyis[now] = "(" + ex + "+" + ey + ")";
need = addf(T, y);
ex = dfs(l, k, need);
if (!ex.empty()) return jiyis[now] = "(" + ex + "-" + ey + ")";
if (y.p != 0) {
if (divf(T, y, need)) {
ex = dfs(l, k, need);
if (!ex.empty()) return jiyis[now] = "(" + ex + "*" + ey + ")";
}
} else if (T.p == 0) {
return jiyis[now] = "(" + all_add(l, k) + "*" + ey + ")";
}
if (y.p != 0) {
need = mulf(T, y);
ex = dfs(l, k, need);
if (!ex.empty()) return jiyis[now] = "(" + ex + "/" + ey + ")";
}
}
}
}
return jiyis[now] = "";
}
bool solve(int n) {
m = 3 * n;
for (int i = 1; i <= n; i++) { // init
a[i * 3 - 2] = 1;
a[i * 3 - 1] = 4;
a[i * 3] = 4;
}
// 只完整预处理长度 <= K 的区间
K = m / 2;
jiyis.clear();
for (int i = 1; i <= m; i++) {
for (int j = i; j <= m; j++) {
dp[i][j].clear();
}
}
for (int i = 1; i <= m; i++) {
dp[i][i][Frac(a[i], 1)] = to_string(a[i]);
}
// 这里只做到 len <= K 而不是 m
for (int len = 2; len <= K; len++) {
for (int l = 1; l + len - 1 <= m; l++) {
int r = l + len - 1;
for (int k = l; k < r; k++) {
for (auto &L : dp[l][k]) {
for (auto &R : dp[k + 1][r]) {
ins(dp[l][r], addf(L.first, R.first), "(" + L.second + "+" + R.second + ")");
ins(dp[l][r], subf(L.first, R.first), "(" + L.second + "-" + R.second + ")");
ins(dp[l][r], mulf(L.first, R.first), "(" + L.second + "*" + R.second + ")");
Frac v;
if (divf(L.first, R.first, v)) {
ins(dp[l][r], v, "(" + L.second + "/" + R.second + ")");
}
}
}
}
}
}
// 定向搜整个区间能否得到 tgt
string ans = dfs(1, m, tgt);
if (!ans.empty()) {
printf("Found min n=%d\nExpr %s\n", n, ans.c_str()); // 不喜欢用 cout
return true;
}
return false;
}
int main() {
for (int n = 1; n <= 8; n++) {
if (solve(n)) return 0;
}
printf("No solution\n");
return 0;
}
(我电脑上还没装一些 c++编辑器 这里顺手用 pycharm 代替了)
$$
{1+(4((4((1+4)((4+(1}{(4+4)))(1+4))))-4))}= 1635
$$
完美!
