O(n ^ 2)在解决这个问题时还不够快。更快的方法?

时间:2012-11-17 23:19:20

标签: java string algorithm

我一直试图在ACM Timus上解决这个问题

http://acm.timus.ru/problem.aspx?space=1&num=1932

我的第一种方法是O(n ^ 2),它肯定不够快,无法通过所有测试。下面的O(n ^ 2)代码给出了测试10的TL。

import java.util.*;
import java.io.*;

public class testtest
{
    public static void main(String[] args) throws IOException
    {
        BufferedReader rr = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(rr.readLine());
        String[] p = new String[n];
        for (int i = 0; i < n; i ++)
        {
            p[i] = rr.readLine();
        }
        int[] diff = new int[]{0, 0, 0, 0};
        for (int i = 0; i < n - 1; i ++)
        {
            for (int j = i + 1; j < n; j ++)
            {
                int ans  = (p[i].charAt(0) == p[j].charAt(0) ? 0 : 1) +
                           (p[i].charAt(1) == p[j].charAt(1) ? 0 : 1) +
                           (p[i].charAt(2) == p[j].charAt(2) ? 0 : 1) +
                           (p[i].charAt(3) == p[j].charAt(3) ? 0 : 1);
                diff[ans - 1] ++;
            }
        }
        System.out.print(diff[0] + " " + diff[1] + " " + diff[2] + " " + diff[3]);
    }
}

有什么想法让这种方法更快?我注意到输入中只允许一组有限的字符('0'..'9','a'..'f')所以我们可以创建数组(内存限制就足够了)来快速检查之前输入了字符。

谢谢......我不需要实际实施,只是快速的想法/想法会很棒。 编辑:感谢您的好主意。我已尝试使用位逻辑对O(n ^ 2)进行改进,但仍超出时间限制。 pascal代码如下。

program Project2;

{$APPTYPE CONSOLE}

var
  i, j, n, k, bits: integer;
  arr: array[1..65536] of integer;
  diff: array[1..4] of integer;
  a, b, c, d: char;

function g(c: char): integer; inline;
begin
  if ((c >= '0') and (c <= '9')) then
  begin
    Result := Ord(c) - 48;
  end
  else
  begin
    Result := Ord(c) - 87;
  end;
end;

begin
  Readln(n);
  for i := 1 to n do
  begin
    Read(a); Read(b); Read(c); Readln(d);
    arr[i] := g(a) * 16 * 16 * 16 + g(b) * 16 * 16 + g(c) * 16 + g(d);
    for j := 1 to i - 1 do
    begin
      bits := arr[i] xor arr[j];
      k := ((bits or (bits shr 1) or (bits shr 2) or (bits shr 3)) and $1111) mod 15;
      Inc(diff[k]);
    end;
  end;
  Write(diff[1], ' ', diff[2], ' ', diff[3], ' ', diff[4]);
{$IFNDEF ONLINE_JUDGE}
  Readln;
{$ENDIF}
end.

所以我想,我会尝试其他更好的建议..

编辑:我已经尝试了Daniel的算法并且很有前途,可能在下面的代码中有一个错误,它在测试10上一直得到错误的答案......有人可以看看吗?非常感谢......

import java.util.*;
import java.io.*;

public class testtest
{
    private static int g(char ch)
    {
        if ((ch >= '0') && (ch <= '9'))
        {
            return (int)ch - 48;
        }
        return (int)ch - 87;
    }

    public static void main(String[] args) throws IOException
    {
        BufferedReader rr = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(rr.readLine());
        int[] p = new int[n];
        int[] all = new int[65536];
        int[][] miss = new int[4][4096];
        int[] g12 = new int[256];
        int[] g13 = new int[256];
        int[] g14 = new int[256];
        int[] g23 = new int[256];
        int[] g24 = new int[256];
        int[] g34 = new int[256];
        int[][] gg = new int[4][16];
        int same3, same2, same1, same0, same4;
        for (int i = 0; i < n; i ++)
        {
            String s = rr.readLine();
            int x = g(s.charAt(0)) * 4096 + g(s.charAt(1)) * 256 + g(s.charAt(2)) * 16 + g(s.charAt(3));
            p[i] = x;
            all[x] ++;
            miss[0][x >> 4] ++;
            miss[1][(x & 0x000F) | ((x & 0xFF00) >> 4)] ++;
            miss[2][(x & 0x00FF) | ((x & 0xF000) >> 4)] ++;
            miss[3][x & 0x0FFF] ++;
            g12[x >> 8] ++;
            g13[((x & 0x00F0) >> 4) | ((x & 0xF000) >> 8)] ++;
            g14[(x & 0x000F) | ((x & 0xF000) >> 8)] ++;
            g23[(x & 0x0FF0) >> 4] ++;
            g24[(x & 0x000F) | ((x & 0x0F00) >> 4)] ++;
            g34[x & 0x00FF] ++;
            gg[0][x >> 12] ++;
            gg[1][(x & 0xF00) >> 8] ++;
            gg[2][(x & 0xF0) >> 4] ++;
            gg[3][x & 0xF] ++;
        }

        same4 = 0;
        for (int i = 0; i < 65536; i ++)
        {
            same4 += (all[i] - 1) * (all[i]) / 2;
        }

        same3 = 0;
        for (int i = 0; i < 4096; i ++)
        {
            same3 += (miss[0][i] - 1) * (miss[0][i]) / 2;
            same3 += (miss[1][i] - 1) * (miss[1][i]) / 2;
            same3 += (miss[2][i] - 1) * (miss[2][i]) / 2;
            same3 += (miss[3][i] - 1) * (miss[3][i]) / 2;
        }

        same2 = 0;
        for (int i = 0; i < 256; i ++)
        {
            same2 += (g12[i] - 1) * g12[i] / 2;
            same2 += (g13[i] - 1) * g13[i] / 2;
            same2 += (g14[i] - 1) * g14[i] / 2;
            same2 += (g23[i] - 1) * g23[i] / 2;
            same2 += (g24[i] - 1) * g24[i] / 2;
            same2 += (g34[i] - 1) * g34[i] / 2;
        }

        same1 = 0;
        for (int i = 0; i < 16; i ++)
        {
            same1 += (gg[0][i] - 1) * gg[0][i] / 2;
            same1 += (gg[1][i] - 1) * gg[1][i] / 2;
            same1 += (gg[2][i] - 1) * gg[2][i] / 2;
            same1 += (gg[3][i] - 1) * gg[3][i] / 2;
        }

        same3 -= 4 * same4;
        same2 -= 6 * same4 + 3 * same3;
        same1 -= 4 * same4 + 3 * same3 + 2 * same2;
        same0 = (int)((long)(n * (n - 1) / 2) - same4 - same3 - same2 - same1);
        System.out.print(same3 + " " + same2 + " " + same1 + " " + same0);
    }
}

修改 终于得到了AC ...感谢Daniel这么好的算法!

9 个答案:

答案 0 :(得分:6)

对于小n,当然,检查每对的蛮力O(n²)算法更快,因此人们希望找到切换算法的良好截止点。在没有测量的情况下,由于不均匀的包络考虑,我预计值在200到3000之间。

将盗版ID解析为int,将其解析为十六进制数。将ID存储在

int[] pirates = new int[n];

首先,计算具有相同ID的盗版对的数量(这里可以省略此步骤,因为问题陈述没有。)

int[] allFour = new int[65536];
for(int i = 0; i < n; ++i) {
    allFour[pirate[i]] += 1;
}
int fourIdentical = 0;
for(int i = 0; i < 65536; ++i) {
    fourIdentical += allFour[i]*(allFour[i] - 1) / 2;
}

接下来,计算他们的ID中有三个相同半字节的盗版对,

int oneTwoThree(int p) {
    return p >> 4;
}
int oneTwoFour(int p) {
    return (p & 0x000F) | ((p & 0xFF00) >> 4);
}
int oneThreeFour(int p) {
    return (p & 0x00FF) | ((p & 0xF000) >> 4);
}
int twoThreeFour(int p) {
    return p & 0x0FFF;
}

int[] noFour  = new int[4096];
int[] noThree = new int[4096];
int[] noTwo   = new int[4096];
int[] noOne   = new int[4096];

for(int i = 0; i < n; ++i) {
    noFour[oneTwoThree(pirate[i])] += 1;
    noThree[oneTwoFour(pirate[i])] += 1;
    noTwo[oneThreeFour(pirate[i])] += 1;
    noOne[twoThreeFour(pirate[i])] += 1;
}

int threeIdentical = 0;
for(int i = 0; i < 4096; ++i) {
    threeIdentical += noFour[i]*(noFour[i]-1) / 2;
}
for(int i = 0; i < 4096; ++i) {
    threeIdentical += noThree[i]*(noThree[i]-1) / 2;
}
for(int i = 0; i < 4096; ++i) {
    threeIdentical += noTwo[i]*(noTwo[i]-1) / 2;
}
for(int i = 0; i < 4096; ++i) {
    threeIdentical += noOne[i]*(noOne[i]-1) / 2;
}

但是,每对具有四个相同半字节的盗版者在这里被计算4 choose 3 = 4次,对于三个半字节的每个可能选择,所以我们需要减去它(好吧,不是问题,而是原理):

threeIdentical -= 4*fourIdentical;

然后,计算他们的ID中有两个相同半字节的海盗对:

int oneTwo(int p) {
    return p >> 8;
}
int oneThree(int p) {
    return ((p & 0x00F0) >> 4) | ((p & 0xF000) >> 8);
}
int oneFour(int p) {
    return (p & 0x000F) | ((p & 0xF000) >> 8);
}
int twoThree(int p) {
    return (p & 0x0FF0) >> 4;
}
int twoFour(int p) {
    return (p & 0x000F) | ((p & 0x0F00) >> 4);
}
int threeFour(int p) {
    return p & 0x00FF;
}

分配六个256 int个数组,并计算地点ab中相应半字节的盗版数量,例如

int twoIdentical = 0;
int[] firstTwo = new int[256];
for(int i = 0; i < n; ++i) {
    firstTwo[oneTwo(pirate[i])] += 1;
}
for(int i = 0; i < 256; ++i) {
    twoIdentical += firstTwo[i]*(firstTwo[i] - 1) / 2;
}
// analogous for the other five possible choices of two nibbles

但是,具有四个相同半字节的对在此计算4 choose 2 = 6次,并且具有三个相同半字节的对已被计为3 choose 2 = 3次,因此我们需要减去

twoIdentical -= 6*fourIdentical + 3*threeIdentical;

接下来,具有一个相同半字节的对的数量。我相信你可以猜到需要四个函数和数组。然后,我们将计算具有四个相同半字节4 choose 1 = 4次的对,具有三个相同半字节3 choose 1 = 3次的对,以及具有两个相同半字节2 choose 1 = 2次的对,所以

oneIdentical -= 4*fourIdentical + 3*threeIdentical + 2*twoIdentical;

最后,没有相同半字节的对的数量是

int noneIdentical = (int)((long)n*(n-1) / 2) - oneIdentical - twoIdentical - threeIdentical - fourIdentical;

(强制转换为long以避免n*(n-1)溢出。

答案 1 :(得分:2)

这可能是比较标识符对的快速方法。它假定两个标识符都是以十六进制数字而不是字符串形式读取的。

int bits = p[i] ^ p[j];
int ans = ((bits | bits >> 1 | bits >> 2 | bits >> 3) & 0x1111) % 15;
diff[ans - 1]++;

答案 2 :(得分:2)

这个怎么样:

创建一个四维整数数组,每个维度为16个元素宽,所有元素都初始化为0。那么,那将是16*16*16*16*4 = 262144个字节,大约是262KB。现在,将每个标识符索引(插入)到正在读取的结构中。在建立索引时,请在每个维度增加counter ...

在索引此结构时,您可以逐步计算所需的答案。

举个例子,假设我正在为标识符 dead 编制索引。在第一个维度上,选择与 d 对应的位置,假设此位置的计数器为n1,这意味着到目前为止已有n1个标识符与 d 位置1.现在取 e 并将其插入第二维上的正确位置...并说明此位置的计数器为n2,然后我确切地知道有n1 - n2个标识符与插入的当前标识符相比在3个位置上有所不同...

编辑:我开始怀疑自己。如果两个标识符在第一个字符中不同但在第二个字符中相等会怎么样?这种方法无法挑选出我认为的那种方法。需要更多的思考......

编辑:暂停“智能解决方案”后,我尝试改进基本的O(n^2)算法,如下所示。

每个标识符有4个十六进制数字,因此可以打包成一个整数(虽然2字节的字就足够了)。所以,这样的整数看起来像:

[ ][ ][ ][ ] * [ ][ ][ ][ ] * [ ][ ][ ][ ] * [ ][ ][ ][ ]

其中每个[ ]代表一个位,每个[ ][ ][ ][ ]组(半字节)对应于标识符中的十六进制数字。现在我们需要一种快速比较其中两个标识符的方法。给定两个标识符ab,首先我们计算c = a ^ b,它将为我们提供两位模式的 XOR (差异,类型)。现在我们将这个位模式c与以下常量模式进行比较:

u = [1][1][1][1] * [0][0][0][0] * [0][0][0][0] * [0][0][0][0]

v = [0][0][0][0] * [1][1][1][1] * [0][0][0][0] * [0][0][0][0]

w = [0][0][0][0] * [0][0][0][0] * [1][1][1][1] * [0][0][0][0]

x = [0][0][0][0] * [0][0][0][0] * [0][0][0][0] * [1][1][1][1]

比较如下:

diffs = 0

if (c & u)
  diffs++
if (c & v)
  diffs++
if (c & w)
  diffs++
if (c & x)
  diffs++

在此操作结束时,diffs将包含ab不同的字符数。请注意,可以实现相同的方法,而无需将标识符打包成整数(即将每个十六进制数字保留在它自己的整数/字符类型上)但我认为这个包装保存的内存可能意味着什么。

恕我直言,这只是对基本O(n^2)算法的一个小改进。如果接受的答案是这种技巧而不是基于更好的算法/数据结构的答案,我会非常失望。急切地等待某人就这样的答案提出一些建议......: - )

答案 3 :(得分:2)

这可以通过id列表单次传递来解决,所以O(n)时间,但你仍需要小心,以便实现及时运行。

考虑找到相等字符串对的问题。这可以通过跟踪先前在地图中找到的每个字符串的数量来完成:

long pairs = 0;
Set<String, Long> map = new HashMap<String, Long>();
for(String id:ids) {
    if(map.containsKey(id)) {
        pairs += map.get(id);
        map.put(id, map.get(id) + 1);
    } else {
        map.put(id, 1);
    }
}

现在找到一个字符不同的对。您可以存储不包括每个字符的字符串。所以对于“abcd”,存储“.bcd”,“a.cd”,“ab.d”和“abc”。在地图上。对于每个id,您可以为每个缺少的字符位置添加每个String的计数。这也将计算相等的字符串,因此减去相等字符串的数量。

使用Inclusion/Exclusion principle概括为更多缺少的字符。例如,s中包含2个不同字符的字符串数量为:

sum of all character positions `x` and `y`:
    the number of strings equal to `s` excluding characters `x` and `y`
    - the number of strings equal to `s` excluding character `x`
    - the number of strings equal to `s` excluding character `y`
    + the number of strings equal to `s`

出于效率原因,您不希望进行字符串操作,因此实际的实现应该将String转换为整数。此外,您可以使用长数组(其中索引表示字符串,而值是匹配字符串的数量)而不是Map。

答案 4 :(得分:1)

不是计算对,而是计算匹配然后计算对?所有以d开头的IE 5代码实际上都是5! = 10双。

这将允许您使用可以在插入时而不是迭代计算位置匹配的结构。

编辑:

当时我正在考虑可能有一个带有根节点和4个嵌套级别的树结构。每个级别代表代码中的一个位置。 IE root - &gt; d - &gt; e - &gt; a - &gt; F。当您插入每个代码时,如果该级别不存在该字符,请插入它,如果它确实计算了它。在每次代码插入结束时,您将知道该代码有多少匹配,然后将其添加到diff中的相应存储桶中。然后通过取n来转换为成对而不仅仅是匹配!每个桶。

答案 5 :(得分:1)

你有n!/(n-2)!2! = O(n 2 )对(n个输入数),无论你做什么,你都需要检查它们,因为这是要求

您可以通过将整个输入插入DAWG并稍后遍历来优化时间。对于DAWG中的任何2个路径,相似性被定义为由两个路径中存在的边缘组成的集合。构造此集合并从路径长度中减去其大小以找到差异距离。

答案 6 :(得分:1)

我们可以将私有数字映射到一个三维矩阵P [x] [y] [z]吗?

  • x是私有数(2≤n≤65536)。
  • y是0到15之间的标识符(0 - 9,a - f)。
  • z将包含标识符(0 - f)的操作。

例如:


牛肉
f00d

相应的p [x] [y]将是

0 1 2 3 4 5 6 7 8 9 a b c d e f

0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0
0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1
1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1

z demension将是

0 0 0 0 0 0 0 0 0 2 0 0 9 4 0(2 - > 0010,9 - > 1001,4 - > 0100)
0 0 0 0 0 0 0 0 0 0 8 0 0 6 1(6 - > 0110,8 - > 1000,1 - > 0001)
6 0 0 0 0 0 0 0 0 0 0 0 1 0 8

我们逐列检查p [x] [y],如果2个盗版者具有相同的标识符,那么我们会做一个“&amp;”位置之间的位操作并保存结果(可能需要另一个矩阵来确定当前列中的这个新匹配之前匹配的2个盗版者的次数,O(1))。

遍历此矩阵一次可能足够O(n)。

尚未通过代码证明,这是一个好主意。希望能帮助到你。

感谢您的解谜。

答案 7 :(得分:1)

这个答案是对@fgb / @Daniel提供的答案的详细说明。

让我们代表每个标识符,如H1H2H3H4(四个十六进制数字)

  1. 我们有四个数组,每个数组的长度为16*16*16 (4096)。这四个数组将用于跟踪3个位置相等的标识符。

  2. 对于读取的每个标识符,计算值(整数)H2H3H4H1H3H4H1H2H4H1H2H3(请注意,这些值的范围为0 - 4095 ;这就是我们选择4096的数组大小的原因。现在,使用这些值中的每一个来索引步骤(1)中提到的四个数组,并将相应的位置增加一个。

  3. 我们有另外六个数组,每个数组的长度为16*16 (256)。这五个数组将用于跟踪2个位置相等的标识符。

  4. 对于读取的每个标识符,计算值(整数)H3H4H2H4H1H4H1H3H1H2,{{1 }}。现在,使用这些值中的每一个来索引步骤(3)中提到的六个数组,并将相应的位置增加一。每个数组位置有效地计算在2个位置相等的标识符的数量(注意,该计数将包括在3个位置相等的标识符的数量)。

  5. 我们还有另一个四个数组,每个数组的宽度为H2H3。这四个数组将用于跟踪在1个位置相等的标识符。

  6. 对于读取的每个标识符,计算值(整数)16H4H3H2。现在,使用这些值中的每一个来索引步骤(5)中提到的四个数组,并将相应的位置增加一。每个数组位置有效地计算在1个位置相等的标识符的数量(同样,该计数将包括等于3个,2个位置的标识符的数量)。

  7. 请注意,所有这些都可以通过输入一次完成。然而,这还不足以计算最终答案,我们必须逐步计算最终答案以及步骤1 - 6.这就是我们如何做到这一点:

    H1
    • 在步骤(2)中,当我们增加某个数组位置时,如果结果值为diff1 = 0 diff2 = 0 diff3 = 0 diff4 = 0 ,则为u (u > 1)。这意味着,使用已找到的diff1 = diff1 + (u - 1)个标识符,我们可以制作(u - 1)个新对(通过将每个对与当前标识符配对)。

    • 在步骤(4)中,当我们增加某个数组位置时,如果结果值为(u - 1),则为v (v - u >= 1)。这意味着,我们知道在上一步中已经考虑了diff2 = diff2 + (v - u)个标识符,因此不应重新计算它们。因此,u 2个位置提供与此标识符相等/不同的其他标识符。使用这些标识符,我们可以制作(v - u)个新对(通过将每个对与当前标识符配对)。

    • 在步骤(6)中,当我们增加某个数组位置时,如果结果值为(v - u),则为w (w - u >= 1)。解释与上面的解释非常相似。

    再次注意,所有这些步骤都可以在整个输入的单次传递过程中完成。

    现在,接下来是如何计算diff3 = diff3 + (w - u)。这可以在步骤(2)中完成,当我们增加一些数组位置时,如果新值为1,则diff4 如果新值为2,则{{1} }。这会跟踪到目前为止尚未找到匹配标识符的新鲜标识符。

    更新:上述方法效果很好,但我建议用于计算diff4 = diff4 + 1的方法不起作用。基本上,diff4 = diff4 - 1不是唯一标识符,而是我们可以通过在所有4个位置组合与每个其他不同的标识符来形成多少对。

    在C:

    中的以下实现中采用了另一种方法(diff4
    diff4

    我用自己的一些样本输入测试了它,它似乎工作。如果OP可以测试它并看它是否有效,那将是很好的。它可能需要更多的工作。

    更新:好的,it works!不得不重新格式化代码,以通过那里的编译器。我还更新了此处列出的代码以反映更改。

答案 8 :(得分:0)

你可以尝试一下这些方面:

当您处理每个条目时,将其添加到名称列表中,该列表在第一个位置具有特定字母。列表中的任何条目已经列入“4个匹配候选者”列表中。

然后将其添加到具有特定第二个字母的名称列表中。此列表中已有的“4个匹配候选者”中的任何条目保留在“4个匹配候选者”中,“4个匹配候选者”中的所有其他条目将被移动到“3个匹配候选者”列表以及此列表中的所有条目。

然后将其添加到具有特定第三个字母的名称列表中。此列表中已有的“3个匹配候选者”中的任何条目仍保留在“3个匹配候选者”中,“3个匹配候选者”中的所有其他条目将被移动到“2个匹配候选者”列表以及此处已有的所有条目名单。此列表中“4个匹配候选者”中的任何条目都保留在“4个匹配候选者”中,“4个匹配候选者”中的所有其他条目将被移动到“3个匹配候选者”列表中。

然后将其添加到具有特定第四个字母的名称列表中。此列表中已有的任何条目将被添加到1个匹配候选者的计数中,“2个匹配候选者”列表中的任何条目将被添加到2个匹配候选者的计数中,“3个匹配候选者”列表中的任何条目都将添加到3个匹配候选人的数量,“4个匹配候选人”列表中的任何条目都被添加到4个匹配候选人的数量中。

这应该(希望)表示您只处理一次列表,而不是检查每个其他条目,您只需要检查在相同位置具有相同字母的子集。

或许我需要去睡觉......

编辑:

所以我昨晚误解了这个目标。上述过程计算相似性而不是差异,但我们可以使用类似的方法来计算差异。基本上保留4个列表用于1个差异词,2个差异词,3个差异词和4个差异词。到目前为止,我们已经看到4个差异词的列表。然后从包含现有名称的列表开始,然后查找首先包含当前单词首字母的单词列表。将此列表中的2个差异列表中的单词移动到一个差异列表,将3个差异列表移动到2个差异列表,将4个差异列表移动到3个差异列表。

处理完所有列表后,您可以将每个列表中的计数添加到全局计数中。

我认为这应该有用,但我会稍后再说。 ..