Java 为什么这个DP算法比蛮力算法慢?

Java 为什么这个DP算法比蛮力算法慢?,java,algorithm,recursion,hashmap,dynamic-programming,Java,Algorithm,Recursion,Hashmap,Dynamic Programming,我正致力于实现最长回文子串问题,我采用了DP和extraO(N^2)(是的,我知道有一种更有效的算法,但我对这篇文章不感兴趣)。 我的实现基本上使用了递归: P(i, j) = P(i + 1, j - 1) ^ s[i] == s[j] 生成相关表,但运行时间比预期慢得多。 如果我在IDE中运行它几秒钟(15+)后,它确实会给出正确的输出,但是任何在线法官都会拒绝它,因为它太慢了。我不确定问题出在哪里,因为我正在使用记忆。因此,不会重新计算相同的情况。 开始显示算法存在性能问题的字符串长度超

我正致力于实现最长回文子串问题,我采用了DP和extra
O(N^2)
(是的,我知道有一种更有效的算法,但我对这篇文章不感兴趣)。
我的实现基本上使用了递归:

P(i, j) = P(i + 1, j - 1) ^ s[i] == s[j]
生成相关表,但运行时间比预期慢得多。
如果我在IDE中运行它几秒钟(15+)后,它确实会给出正确的输出,但是任何在线法官都会拒绝它,因为它太慢了。我不确定问题出在哪里,因为我正在使用记忆。因此,不会重新计算相同的情况。
开始显示算法存在性能问题的字符串长度超过900个字符

更新
我正在更新问题以添加完整的源代码和测试用例

动态规划方法O(N^2)时间和O(N^2)空间(不可接受且速度太慢)

测试和输入:

public static void main(String[] args) {
        final String string1 = "civilwartestingwhetherthatnaptionoranynartionsoconceivedandsodedicatedcanlongendureWeareqmetonagreatbattlefiemldoftzhatwarWehavecometodedicpateaportionofthatfieldasafinalrestingplaceforthosewhoheregavetheirlivesthatthatnationmightliveItisaltogetherfangandproperthatweshoulddothisButinalargersensewecannotdedicatewecannotconsecratewecannothallowthisgroundThebravelmenlivinganddeadwhostruggledherehaveconsecrateditfaraboveourpoorponwertoaddordetractTgheworldadswfilllittlenotlenorlongrememberwhatwesayherebutitcanneverforgetwhattheydidhereItisforusthelivingrathertobededicatedheretotheulnfinishedworkwhichtheywhofoughtherehavethusfarsonoblyadvancedItisratherforustobeherededicatedtothegreattdafskremainingbeforeusthatfromthesehonoreddeadwetakeincreaseddevotiontothatcauseforwhichtheygavethelastpfullmeasureofdevotionthatweherehighlyresolvethatthesedeadshallnothavediedinvainthatthisnationunsderGodshallhaveanewbirthoffreedomandthatgovernmentofthepeoplebythepeopleforthepeopleshallnotperishfromtheearth";
        //final String string2 = "ibvjkmpyzsifuxcabqqpahjdeuzaybqsrsmbfplxycsafogotliyvhxjtkrbzqxlyfwujzhkdafhebvsdhkkdbhlhmaoxmbkqiwiusngkbdhlvxdyvnjrzvxmukvdfobzlmvnbnilnsyrgoygfdzjlymhprcpxsnxpcafctikxxybcusgjwmfklkffehbvlhvxfiddznwumxosomfbgxoruoqrhezgsgidgcfzbtdftjxeahriirqgxbhicoxavquhbkaomrroghdnfkknyigsluqebaqrtcwgmlnvmxoagisdmsokeznjsnwpxygjjptvyjjkbmkxvlivinmpnpxgmmorkasebngirckqcawgevljplkkgextudqaodwqmfljljhrujoerycoojwwgtklypicgkyaboqjfivbeqdlonxeidgxsyzugkntoevwfuxovazcyayvwbcqswzhytlmtmrtwpikgacnpkbwgfmpavzyjoxughwhvlsxsgttbcyrlkaarngeoaldsdtjncivhcfsaohmdhgbwkuemcembmlwbwquxfaiukoqvzmgoeppieztdacvwngbkcxknbytvztodbfnjhbtwpjlzuajnlzfmmujhcggpdcwdquutdiubgcvnxvgspmfumeqrofewynizvynavjzkbpkuxxvkjujectdyfwygnfsukvzflcuxxzvxzravzznpxttduajhbsyiywpqunnarabcroljwcbdydagachbobkcvudkoddldaucwruobfylfhyvjuynjrosxczgjwudpxaqwnboxgxybnngxxhibesiaxkicinikzzmonftqkcudlzfzutplbycejmkpxcygsafzkgudy";

        long startTime = System.nanoTime();

        String palindromic = longestPalindromeDP(string1);
        long elapsed = TimeUnit.SECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);

        System.out.println(elapsed);
        System.out.println(palindromic);

    }
蛮力在0秒内完成。
动态编程最长可在9秒内完成(取决于机器)

这里有什么问题?
我知道可以通过一些优化来提高性能,但既然我使用了记忆化,O(N^3)的性能怎么可能优于O(N^2)

更新
根据@CahidEnesKeleş的回答进行更新

我用自定义对象替换了
列表
作为键:

class IdxPair {  
   int i;
   int j;  
   IdxPair(int i, int j) {
     this.i = i;
     this.j = j;  
   }  

   @Override  
   public boolean equals(Object o) {  
      if(o == null || !(o instanceof IdxPair)) return false;  
      if(this == o ) return true;  
      IdxPair other = (IdxPair) o;  
      return this.i == other.i && this.j == other.j;  
   }  

   @Override 
   public int hashCode() {  
     int h = 31;  
     h = 31 * i + 37;  
     h = (37 * h) + j;    
     return h;  
   }  
} 

尽管之前有几个测试案例失败,但现在通过了,总体速度仍然太慢,并且被在线评委拒绝。

我尝试使用类似c的数组,而不是
HashMap
,下面是代码:

public static String longestPalindromeDP(String s) {
    int[][] cache = new int[s.length()][s.length()];
    for (int i = 0; i < s.length(); i++) {
        for (int j = 0; j < s.length(); j++) {
            cache[i][j] = -1;
        }
    }
    for(int i = 0; i < s.length(); i++) {
        for(int j = 0; j < s.length(); j++) {
            populateTable(s, i, j, cache);
        }
    }
    int start = 0;
    int end = 0;
    for(int i = 0; i < s.length(); i++) {
        for(int j = 0; j < s.length(); j++) {
            if(cache[i][j] == 1) {
                if(Math.abs(start - end) < Math.abs(i - j)) {
                    start = i;
                    end = j;
                }
            }
        }
    }
    return s.substring(start, end + 1);
}

private static boolean populateTable(String s, int i, int j, int[][] cache) {
    if(i == j) {
        cache[i][j] = 1;
        return true;
    }
    if(Math.abs(i - j) == 1) {
        cache[i][j] = s.charAt(i) == s.charAt(j) ? 1 : 0;
        return s.charAt(i) == s.charAt(j);
    }

    if (cache[i][j] != -1) {
        return cache[i][j] == 1;
    }

    boolean res = populateTable(s, i + 1, j - 1, cache) && s.charAt(i) == s.charAt(j);
    cache[i][j] = res ? 1 : 0;
    cache[j][i] = res ? 1 : 0;
    return res;
}
此代码在101毫秒内完成

Map<Integer, Boolean> cache = new HashMap<>();
for (int i = 0; i < 1000; i++) {
    for (int j = 0; j < 1000; j++) {
        cache.put(i*1000 + j, true);
    }
}
不,这也很快。所以散列值在大多数情况下可能会发生冲突。为了测试这一点,我将所有哈希代码放在一个集合中,并检查其大小

Set<Integer> hashSet = new HashSet<>();
for (int i = 0; i < 1000; i++) {
    for (int j = 0; j < 1000; j++) {
        hashSet.add(Arrays.asList(i, j).hashCode());
    }
}
System.out.println(hashSet.size());
Set hashSet=new hashSet();
对于(int i=0;i<1000;i++){
对于(int j=0;j<1000;j++){
add(Arrays.asList(i,j).hashCode());
}
}
System.out.println(hashSet.size());

结果是31969。1000000人中的31969人约为%3,2。我认为这是缓慢的根源。1m项对于HashMap来说太多了。当越来越多的碰撞发生时,它开始远离O(1)。

为什么更好的算法与此无关?对2和73与73和2完全相同,所以你要做两次相同的工作。因此,最简单的改进是:从
i
迭代
j
,而不是从
0
迭代@CiaPan:是的,但我缓存它们,因此我从不重新计算它们。检查cache.put(新对(j,i),结果);//再加上反面你认为这仍然是一个问题吗?@LouisWasserman:我的意思是我想理解这种方法中的问题,而不是关注我是否应该遵循另一种更优化的方法,例如Manacher算法或从center@LouisWasserman:据我所知,从性能角度来看,应用额外的空间表示良好,即O(N^2)。这是一个很好的观察结果,但这意味着碰撞会影响
containsKey
get
。但在测量altgough时,我确实注意到性能在波动,范围在50毫秒到400毫秒之间。我认为这是由于每个桶的列表很小,因此仍然保持在~O(1)。这就是您所期望的吗?请参阅我文章中的更新这个新类将运行时间减少了10倍多,但仍然比类c的数组实现慢10倍(请参阅我答案中的第一段代码)。我假设这是因为HashMap使用了额外的操作和条件,因为类c数组允许即时访问。我测试了这两个,就像我的答案中的测试一样,确实有10倍的运行时间因子。O(N³)算法优于O(N²)算法的另一个原因是隐藏常数因子。Bruteforce算法的常数因子非常小。但通过一些优化,O(N²)算法可以变得更快。
1m项对于HashMap来说太多了
1ms是如何产生的?
for (int i = 0; i < 1000; i++) {
    for (int j = 0; j < 1000; j++) {
        cache.put(Arrays.asList(i, j), true);
    }
}
for (int i = 0; i < 1000; i++) {
    for (int j = 0; j < 1000; j++) {
        Arrays.asList(i, j);
    }
}
Map<Integer, Boolean> cache = new HashMap<>();
for (int i = 0; i < 1000; i++) {
    for (int j = 0; j < 1000; j++) {
        cache.put(i*1000 + j, true);
    }
}
for (int i = 0; i < 1000; i++) {
    for (int j = 0; j < 1000; j++) {
        Arrays.asList(i, j).hashCode();
    }
}
Set<Integer> hashSet = new HashSet<>();
for (int i = 0; i < 1000; i++) {
    for (int j = 0; j < 1000; j++) {
        hashSet.add(Arrays.asList(i, j).hashCode());
    }
}
System.out.println(hashSet.size());