Observe que isso é pelo menos tão difícil quanto contar o número de triângulos em um determinado gráfico. Se sua matriz de entrada codifica um gráfico como "0" indica uma aresta e "1" indica uma aresta ausente, então se e somente se houver é um triângulo formado pelos nós , e , e caso contrário, é . max(aij,aik,ajk)=0ijk1
Jukka Suomela
11
Eu acho que os únicos algoritmos significativamente subcúbicos conhecidos para a contagem de triângulos são baseados na rápida multiplicação de matrizes? Pode ser complicado aplicar essas técnicas aqui neste problema. Além disso, se você estiver procurando algo prático, qualquer coisa baseada na multiplicação rápida de matrizes não será útil.
Jukka Suomela
Respostas:
3
Existe uma abordagem bastante prática que funciona em , em que é o número de bits na palavra do processador. A idéia principal é que você itere sobre os elementos da matriz um por um na ordem crescente (rompe arbitrariamente) e "os liga". Considere o momento em que o elemento maior de alguns triplos está ativado. Para simplificar, vamos assumir que o referido elemento é . É natural adicionar o valor do triplo à resposta agora, quando o último elemento estiver ativado. Portanto, temos que contar o número de possíveis , de modo que eO(n3/w)waij,aik,ajkaijkaikajkjá estão ativados (esse seria o número de triplos, aqui é o maior elemento, portanto, eles foram completamente ativados agora). Aqui, podemos acelerar a implementação ingênua de usando a otimização de bits.aijO(n)
Para obter detalhes, você pode consultar a seguinte implementação no C ++ 11 que deve funcionar para ,
(não é muito otimizado; no entanto, ainda supera o somatório ingênuo de por grande margem, pelo menos na minha máquina).n⩽5000|aij|⩽109n=5000
// code is not very elegant,
// but should be understandable
// here the matrix a has dimensions n x n
// a has to be symmetric!
int64_t solve (int n, const vector<vector<int32_t>> &a)
{
std::vector<boost::dynamic_bitset<int64_t>> mat
(n, boost::dynamic_bitset<int64_t>(n));
vector<pair<int, int>> order;
for (int j = 1; j < n; j++)
for (int i = 0; i < j; i++)
order.emplace_back(i, j);
sort(order.begin(), order.end(),
[&] (const pair<int, int> &l, const pair<int, int> &r)
{return a[l.first][l.second] < a[r.first][r.second];});
int64_t ans = 0;
for (const auto &position : order)
{
int i, j;
tie (i, j) = position;
mat[i][j] = mat[j][i] = 1;
// here it is important that conditions
// mat[i][i] = 0 and mat[j][j] = 0 always hold
ans += (mat[i] & mat[j]).count() * int64_t(a[i][j]);
}
return ans;
}
Se você considerar o uso de trapaça nas otimizações de bits, poderá usar o método quatro russos para o mesmo resultado aqui, produzindo um algoritmo , que deve ser menos prático (porque é bastante grande no hardware mais moderno) mas é teoricamente melhor. De fato, vamos escolher e manter cada linha da matriz como uma matriz de inteiros de a , onde o ésimo número em a matriz corresponde aos bits da linha que variam de inclusivo a exclusivo emO(n3/logn)wb≈log2n⌈nb⌉02b−1iibmin(n,(i+1)b)0-indexação. Podemos pré-calcular os produtos escalares de cada dois desses blocos no tempo . A atualização de uma posição na matriz é rápida porque estamos alterando apenas um número inteiro. Para encontrar o produto escalar de linhas e apenas iterar sobre matrizes correspondente a esse linhas, procure produtos escalares dos blocos correspondentes na tabela e resumir os produtos obtidos.O(22bb)ij
O parágrafo acima pressupõe que operações com números inteiros levam tempo . É uma suposição bastante comum , porque geralmente não altera a velocidade comparativa dos algoritmos (por exemplo, se não usarmos essa suposição, o método da força bruta realmente funciona no tempo
(aqui medimos o tempo em operações de bit) se receber valores inteiros com valores absolutos pelo menos até para algumas constantes
(e caso contrário, podemos resolver o problema com multiplicações de matriz de qualquer maneira); no entanto, o método dos quatro russos sugerido acima usa⩽nO(1)O(n3logn)aijnεε>0O(nε)O(n3/logn) operações com números de tamanho nesse caso; portanto, ele faz operações de bits, o que ainda é melhor que a força bruta, apesar da mudança do modelo).O(logn)O(n3)
A questão sobre a existência da abordagem ainda é interessante, no entanto.O(n3−ε)
As técnicas (otimizações de bits e método dos quatro russos) apresentadas nesta resposta não são de forma alguma originais e são apresentadas aqui para a completude da exposição. No entanto, encontrar uma maneira de aplicá-las não era trivial.
Em primeiro lugar, sua sugestão realmente parece ser útil em termos práticos; talvez eu tente no meu caso de uso. Obrigado! Em segundo lugar, a complexidade computacional dos algoritmos ainda é para qualquer tipo numérico de largura fixa. Você poderia elaborar sobre a abordagem ? Eu não entendo como poderíamos encontrar o produto escalar e mais rápido que (o que seria necessário se acessarmos todos os seus elementos). O(n3)O(n3/logn)mat[i]mat[j]O(n)
user89217
Além disso, seu código não define o matque parece ser importante. Eu entendo como isso pode ser definido, mas me pergunto se (mat[i] & mat[j]).count()funcionaria como desejado com qualquer contêiner STL.
user89217
11
Em relação mat- eu acho que devemos usar std::vector<boost::dynamic_bitset<int64_t>>.
user89217
Em relação a mat: sim, eu tinha em mente o conjunto de bits padrão, mas boost::dynamic_bitseté ainda melhor neste caso, porque seu tamanho não precisa ser constante em tempo de compilação. Editará a resposta para adicionar esse detalhe e esclarecer a abordagem dos quatro russos.
precisa
11
Ótimo, isso parece sólido para mim. Um ponto secundário: como o modelo transdicotômico assume que podemos executar operações com palavras de máquina em , não há necessidade de pré-calcular nenhum produto escalar. De fato, o modelo assume que , então é pelo menos tão bom quanto . E, como você diz, pré-calcular produtos escalares não faz sentido prático (uma consulta de matriz será mais lenta que a operação binária). O(1)w≥log2nO(n3/w)O(n3/logn)
Respostas:
Existe uma abordagem bastante prática que funciona em , em que é o número de bits na palavra do processador. A idéia principal é que você itere sobre os elementos da matriz um por um na ordem crescente (rompe arbitrariamente) e "os liga". Considere o momento em que o elemento maior de alguns triplos está ativado. Para simplificar, vamos assumir que o referido elemento é . É natural adicionar o valor do triplo à resposta agora, quando o último elemento estiver ativado. Portanto, temos que contar o número de possíveis , de modo que eO(n3/w) w aij,aik,ajk aij k aik ajk já estão ativados (esse seria o número de triplos, aqui é o maior elemento, portanto, eles foram completamente ativados agora). Aqui, podemos acelerar a implementação ingênua de usando a otimização de bits.aij O(n)
Para obter detalhes, você pode consultar a seguinte implementação no C ++ 11 que deve funcionar para , (não é muito otimizado; no entanto, ainda supera o somatório ingênuo de por grande margem, pelo menos na minha máquina).n⩽5000 |aij|⩽109 n=5000
Se você considerar o uso de trapaça nas otimizações de bits, poderá usar o método quatro russos para o mesmo resultado aqui, produzindo um algoritmo , que deve ser menos prático (porque é bastante grande no hardware mais moderno) mas é teoricamente melhor. De fato, vamos escolher e manter cada linha da matriz como uma matriz de inteiros de a , onde o ésimo número em a matriz corresponde aos bits da linha que variam de inclusivo a exclusivo emO(n3/logn) w b≈log2n ⌈nb⌉ 0 2b−1 i ib min(n,(i+1)b) 0 -indexação. Podemos pré-calcular os produtos escalares de cada dois desses blocos no tempo . A atualização de uma posição na matriz é rápida porque estamos alterando apenas um número inteiro. Para encontrar o produto escalar de linhas e apenas iterar sobre matrizes correspondente a esse linhas, procure produtos escalares dos blocos correspondentes na tabela e resumir os produtos obtidos.O(22bb) i j
O parágrafo acima pressupõe que operações com números inteiros levam tempo . É uma suposição bastante comum , porque geralmente não altera a velocidade comparativa dos algoritmos (por exemplo, se não usarmos essa suposição, o método da força bruta realmente funciona no tempo (aqui medimos o tempo em operações de bit) se receber valores inteiros com valores absolutos pelo menos até para algumas constantes (e caso contrário, podemos resolver o problema com multiplicações de matriz de qualquer maneira); no entanto, o método dos quatro russos sugerido acima usa⩽n O(1) O(n3logn) aij nε ε>0 O(nε) O(n3/logn) operações com números de tamanho nesse caso; portanto, ele faz operações de bits, o que ainda é melhor que a força bruta, apesar da mudança do modelo).O(logn) O(n3)
A questão sobre a existência da abordagem ainda é interessante, no entanto.O(n3−ε)
As técnicas (otimizações de bits e método dos quatro russos) apresentadas nesta resposta não são de forma alguma originais e são apresentadas aqui para a completude da exposição. No entanto, encontrar uma maneira de aplicá-las não era trivial.
fonte
mat[i]
mat[j]
mat
que parece ser importante. Eu entendo como isso pode ser definido, mas me pergunto se(mat[i] & mat[j]).count()
funcionaria como desejado com qualquer contêiner STL.mat
- eu acho que devemos usarstd::vector<boost::dynamic_bitset<int64_t>>
.mat
: sim, eu tinha em mente o conjunto de bits padrão, masboost::dynamic_bitset
é ainda melhor neste caso, porque seu tamanho não precisa ser constante em tempo de compilação. Editará a resposta para adicionar esse detalhe e esclarecer a abordagem dos quatro russos.