在 Java 中优化递归函数
Optimizing Recursive Function in Java
我在优化函数 AltOper 时遇到问题。在集合 {a, b, c} 中,存在不遵循结合律的给定乘法(或二项式运算,无论如何)。 AltOper 获取由 a、b、c 组成的字符串,例如 "abbac",并计算操作的任何可能答案,例如 ((ab)b)(ac) = c, (a(b(ba))) c = a。 AltOper 将每个以 a、b、c 和 return 结尾的操作(不重复)计算为一个三元组。
虽然此代码对于小输入运行良好,但对于有点笨重的输入却需要太多时间。我尝试对一些小的进行记忆,但显然这还不够。折腾了几个小时,终于想通了,它的时间复杂度基本上太大了。但是我找不到更好的算法来计算这个。任何人都可以提出增强(显着)或重建代码的想法吗?无需具体,但只是模糊的想法也会有所帮助。
public long[] AltOper(String str){
long[] triTuple = new long[3]; // result: {number-of-a, number-of-b, number-of-c}
if (str.length() == 1){ // Ending recursion condition
if (str.equals("a")) triTuple[0]++;
else if (str.equals("b")) triTuple[1]++;
else triTuple[2]++;
return triTuple;
}
String left = "";
String right = str;
while (right.length() > 1){
// splitting string into two, by one character each
left = left + right.substring(0, 1);
right = right.substring(1, right.length());
long[] ltemp = AltOper(left);
long[] rtemp = AltOper(right);
// calculating possible answers from left/right split strings
triTuple[0] += ((ltemp[0] + ltemp[1]) * rtemp[2] + ltemp[2] * rtemp[0]);
triTuple[1] += (ltemp[0] * rtemp[0] + (ltemp[0] + ltemp[1]) * rtemp[1]);
triTuple[2] += (ltemp[1] * rtemp[0] + ltemp[2] * (rtemp[1] + rtemp[2]));
}
return triTuple;
}
提前评论:我会修改签名以允许二进制字符串操作,这样您就可以轻松修改您的"input operation"。
java public long[] AltOper(BiFunction<long[], long[], long[]> op, String str) {
我建议对您已经回答的子部分使用某种查找 table。你暗示你已经试过了:
I tried memoization for some small ones, but apparently it's not enough
我想知道出了什么问题,因为这是个好主意,尤其是因为您的输入是字符串,它们可以快速散列和比较,所以将它们放在地图中很便宜。您只需要确保地图不会阻塞整个内存,方法是确保删除旧的、未使用的条目。类似缓存的地图可以做到这一点。我留给你找到一个适合你个人喜好的。
从那里,我会 运行 通过缓存检查进行任何递归,以在地图中找到预先计算的结果。然后会快速查找通常会被疯狂计算的小子串,这会大大降低您的算法的成本。
我稍微重写了您的代码,以允许各种输入(包括不同的操作):
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiFunction;
import org.junit.jupiter.api.Test;
public class StringOpExplorer {
@Test
public void test() {
BiFunction<long[], long[], long[]> op = (left, right) -> {
long[] r = new long[3];
r[0] += ((left[0] + left[1]) * right[2] + left[2] * right[0]);
r[1] += (left[0] * right[0] + (left[0] + left[1]) * right[1]);
r[2] += (left[1] * right[0] + left[2] * (right[1] + right[2]));
return r;
};
long[] result = new StringOpExplorer().opExplore(op, "abcacbabc");
System.out.println(Arrays.toString(result));
}
@SuppressWarnings("serial")
final LinkedHashMap<String, long[]> cache = new LinkedHashMap<String, long[]>() {
@Override
protected boolean removeEldestEntry(final Map.Entry<String, long[]> eldest) {
return size() > 1_000_000;
}
};
public long[] opExplore(BiFunction<long[], long[], long[]> op, String input) {
// if the input is length 1, we return.
int length = input.length();
if (length == 1) {
long[] result = new long[3];
if (input.equals("a")) {
++result[0];
} else if (input.equals("b")) {
++result[1];
} else if (input.equals("c")) {
++result[2];
}
return result;
}
// This will check, if the result is already known.
long[] result = cache.get(input);
if (result == null) {
// This will calculate the result, if it is not yet known.
result = applyOp(op, input);
cache.put(input, result);
}
return result;
}
public long[] applyOp(BiFunction<long[], long[], long[]> op, String input) {
long[] result = new long[3];
int length = input.length();
for (int i = 1; i < length; ++i) {
// This might be easier to read...
String left = input.substring(0, i);
String right = input.substring(i, length);
// Subcalculation.
long[] leftResult = opExplore(op, left);
long[] rightResult = opExplore(op, right);
// apply operation and add result.
long[] operationResult = op.apply(leftResult, rightResult);
for (int d = 0; d < 3; ++d) {
result[d] += operationResult[d];
}
}
return result;
}
}
重写的想法是引入缓存并将操作与探索隔离开来。毕竟你的算法本身就是一个运算,而不是'operation under test'。所以现在你可以(理论上)通过更改 BiFunction
参数来测试任何操作。
这个结果非常快,虽然我真的很怀疑适用性...
我在优化函数 AltOper 时遇到问题。在集合 {a, b, c} 中,存在不遵循结合律的给定乘法(或二项式运算,无论如何)。 AltOper 获取由 a、b、c 组成的字符串,例如 "abbac",并计算操作的任何可能答案,例如 ((ab)b)(ac) = c, (a(b(ba))) c = a。 AltOper 将每个以 a、b、c 和 return 结尾的操作(不重复)计算为一个三元组。
虽然此代码对于小输入运行良好,但对于有点笨重的输入却需要太多时间。我尝试对一些小的进行记忆,但显然这还不够。折腾了几个小时,终于想通了,它的时间复杂度基本上太大了。但是我找不到更好的算法来计算这个。任何人都可以提出增强(显着)或重建代码的想法吗?无需具体,但只是模糊的想法也会有所帮助。
public long[] AltOper(String str){
long[] triTuple = new long[3]; // result: {number-of-a, number-of-b, number-of-c}
if (str.length() == 1){ // Ending recursion condition
if (str.equals("a")) triTuple[0]++;
else if (str.equals("b")) triTuple[1]++;
else triTuple[2]++;
return triTuple;
}
String left = "";
String right = str;
while (right.length() > 1){
// splitting string into two, by one character each
left = left + right.substring(0, 1);
right = right.substring(1, right.length());
long[] ltemp = AltOper(left);
long[] rtemp = AltOper(right);
// calculating possible answers from left/right split strings
triTuple[0] += ((ltemp[0] + ltemp[1]) * rtemp[2] + ltemp[2] * rtemp[0]);
triTuple[1] += (ltemp[0] * rtemp[0] + (ltemp[0] + ltemp[1]) * rtemp[1]);
triTuple[2] += (ltemp[1] * rtemp[0] + ltemp[2] * (rtemp[1] + rtemp[2]));
}
return triTuple;
}
提前评论:我会修改签名以允许二进制字符串操作,这样您就可以轻松修改您的"input operation"。
java public long[] AltOper(BiFunction<long[], long[], long[]> op, String str) {
我建议对您已经回答的子部分使用某种查找 table。你暗示你已经试过了:
I tried memoization for some small ones, but apparently it's not enough
我想知道出了什么问题,因为这是个好主意,尤其是因为您的输入是字符串,它们可以快速散列和比较,所以将它们放在地图中很便宜。您只需要确保地图不会阻塞整个内存,方法是确保删除旧的、未使用的条目。类似缓存的地图可以做到这一点。我留给你找到一个适合你个人喜好的。
从那里,我会 运行 通过缓存检查进行任何递归,以在地图中找到预先计算的结果。然后会快速查找通常会被疯狂计算的小子串,这会大大降低您的算法的成本。
我稍微重写了您的代码,以允许各种输入(包括不同的操作):
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiFunction;
import org.junit.jupiter.api.Test;
public class StringOpExplorer {
@Test
public void test() {
BiFunction<long[], long[], long[]> op = (left, right) -> {
long[] r = new long[3];
r[0] += ((left[0] + left[1]) * right[2] + left[2] * right[0]);
r[1] += (left[0] * right[0] + (left[0] + left[1]) * right[1]);
r[2] += (left[1] * right[0] + left[2] * (right[1] + right[2]));
return r;
};
long[] result = new StringOpExplorer().opExplore(op, "abcacbabc");
System.out.println(Arrays.toString(result));
}
@SuppressWarnings("serial")
final LinkedHashMap<String, long[]> cache = new LinkedHashMap<String, long[]>() {
@Override
protected boolean removeEldestEntry(final Map.Entry<String, long[]> eldest) {
return size() > 1_000_000;
}
};
public long[] opExplore(BiFunction<long[], long[], long[]> op, String input) {
// if the input is length 1, we return.
int length = input.length();
if (length == 1) {
long[] result = new long[3];
if (input.equals("a")) {
++result[0];
} else if (input.equals("b")) {
++result[1];
} else if (input.equals("c")) {
++result[2];
}
return result;
}
// This will check, if the result is already known.
long[] result = cache.get(input);
if (result == null) {
// This will calculate the result, if it is not yet known.
result = applyOp(op, input);
cache.put(input, result);
}
return result;
}
public long[] applyOp(BiFunction<long[], long[], long[]> op, String input) {
long[] result = new long[3];
int length = input.length();
for (int i = 1; i < length; ++i) {
// This might be easier to read...
String left = input.substring(0, i);
String right = input.substring(i, length);
// Subcalculation.
long[] leftResult = opExplore(op, left);
long[] rightResult = opExplore(op, right);
// apply operation and add result.
long[] operationResult = op.apply(leftResult, rightResult);
for (int d = 0; d < 3; ++d) {
result[d] += operationResult[d];
}
}
return result;
}
}
重写的想法是引入缓存并将操作与探索隔离开来。毕竟你的算法本身就是一个运算,而不是'operation under test'。所以现在你可以(理论上)通过更改 BiFunction
参数来测试任何操作。
这个结果非常快,虽然我真的很怀疑适用性...