Soluções de Problemas de Programação com Algoritmos Eficientes

A - Problema do Tapete

Este problema utiliza a técnica de soma de prefixos com diferenças em duas dimensões. Definimos \(s_{i,j}\) como o valor da célula atual e simulamos a operação descrita no enunciado usando diferenças e somas prefixas.

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e3 + 10, mod = 1e9 + 7;
int grade[N][N];
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int dimensao, operacoes; cin >> dimensao >> operacoes;
    for(int i = 1; i <= operacoes; i++){
        int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2;
        grade[x1][y1] ++, grade[x1][y2 + 1] --, grade[x2 + 1][y1] --, grade[x2 + 1][y2 + 1] ++;
    }
    for(int i = 1; i <= dimensao; i++){
        for(int j = 1; j <= dimensao; j++){
            grade[i][j] = grade[i][j] + grade[i - 1][j] + grade[i][j - 1] - grade[i - 1][j - 1];
            cout << grade[i][j] << ' ';
        }
        cout << '\n';
    }
    return 0;
}


B - Árvore FBI

Simulando o enunciado do problema, primeiro ajustamos o número de nós para o comprimento adequado \(2^n\). Realizamos duas travessias em profundidade (dfs). A primeira é similar a uma árvore de segmentos, armazenando informações dos nós e intervalos. Note que mesmo se os dois filhos forem iguais, o valor pode ser 'F' se um deles for 'F'. Na segunda dfs, verificamos primeiro os nós folha, depois os filhos esquerdo e direito, e finalmente o nó raiz.

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
struct no{
    int esquerda, direita;
    char valor;
};
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n; cin >> n;
    n = 1 << n;
    string sequencia; cin >> sequencia;
    sequencia = " " + sequencia;
    vector<no> arvore(n * 4 + 1);
    function<void(int, int, int)> dfs1;
    dfs1 = [&](int u, int l, int r) -> void{
        if(l == r){
            if(sequencia[l] == '1') arvore[u] = {l, r, 'I'};
            else if(sequencia[l] == '0') arvore[u] = {l, r, 'B'};
            else arvore[u] = {l, r, 'F'};
            return;
        }
        arvore[u] = {l, r};
        int meio = l + r >> 1;
        dfs1(u << 1, l, meio), dfs1(u << 1 | 1, meio + 1, r);
        if(arvore[u << 1].valor == arvore[u << 1 | 1].valor){
            if(arvore[u << 1].valor == 'I') arvore[u].valor = 'I';
            else if(arvore[u << 1].valor == 'B') arvore[u].valor = 'B';
            else arvore[u].valor = 'F';
        }
        else arvore[u].valor = 'F';
    };
    function<void(int)> dfs2;
    dfs2 = [&](int u) -> void{
        if(arvore[u].esquerda == arvore[u].direita){
            cout << arvore[u].valor;
            return;
        }
        dfs2(u << 1), dfs2(u << 1 | 1);
        cout << arvore[u].valor; 
    };
    dfs1(1, 1, n);
    dfs2(1);
    return 0;
}


C - Problema das Vacas

Cada vaca olha para a primeira vaca à sua direita que é mais alta que ela. Isso é equivalente a encontrar o primeiro número crescente ao olhar da esquerda para a direita. Podemos usar uma pilha monotônica, processando da direita para a esquerda com uma pilha crescente. A pilha armazena os índices das vacas em ordem crescente de altura, funcionando como uma ponte. Se a vaca atual for mais alta que o topo da pilha, removemos o topo. Se a pilha estiver vazia, não há vaca mais alta à direita. Caso contrário, a vaca no topo da pilha é a primeira mais alta à direita, e então adicionamos a vaca atual à pilha.

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+10, mod = 1e9 + 7;
signed main()
{
    int n; cin >> n;
    vector<int> altura(n + 1);
    for(int i = 1; i <= n; i++) cin >> altura[i];
    stack<int> pilha;
    vector<int> resultado(n + 1);
    for(int i = n; i >= 1; i--){
        while(!pilha.empty() && altura[i] >= altura[pilha.top()]) pilha.pop();
        if(pilha.empty()) resultado[i] = 0;
        else resultado[i] = pilha.top();
        pilha.push(i);
    }
    for(int i = 1; i <= n; i++) cout << resultado[i] << '\n';
    return 0;
}


D - Minimizando a Diferença

Consideramos uma abordagem gulosa. Primeiro, ordenamos o array, pois isso facilita encontrar os valores extremos. Para minimizar a diferença entre o máximo e o mínimo, removemos elementos das duas extremidades. O que resta é um intervalo contínuo. Podemos enumerar esse intervalo. Se estivermos na posição \(i\), a posição final é \(i + n - k - 1\). Precisamos encontrar:

[\min_{1 \le i \le N, i + N - K - 1 \le N} (A_{i + N - K - 1} - A_{i}) ]

Calculamos diretamente, com complexidade de tempo \(O(n \log n)\).

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n, k; cin >> n >> k;
    vector<int> valores(n + 1);
    for(int i = 1; i <= n; i++) cin >> valores[i];
    sort(valores.begin() + 1, valores.end());
    if(k >= valores.size()) return cout << 0 << '\n', 0;
    int resposta = 1e10;
    for(int i = 1; i <= k + 1; i++){
        resposta = min(resposta, valores[i + n - k - 1] - valores[i]);
    }
    cout << resposta << '\n';
    return 0;
}


E - Custo de Percursos em Cidades

Analisando caminhos adjacentes, podemos细分 \(a ➡ b\) as cidades percorridas. Como as cidades são fixas, usamos somas prefixas para resolver o problema. Para cada caminho, calculamos o custo entre cidades adjacentes. Usamos duas somas prefixas: uma para a direção esquerda-direita (\(soma1\)) e outra para direita-esquerda (\(soma2\)).

  • Se a distância entre cidades adjacentes atender à primeira condição, o custo é \(1\)
  • Caso contrário, o custo é \(|a_{i + 1} - a_i|\)

Por exemplo, para o caminho \(2 ➡ 4\), consideramos \(2 ➡ 3\) e depois \(3 ➡ 4\), usando uma abordagem gulosa para obter a solução ótima.

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+10, mod = 1e9 + 7;
void resolver(){
    int n; cin >> n;
    vector<int> cidades(n + 10), soma1(n + 10, 0), soma2(n + 10, 0);
    for(int i = 1; i <= n; i++) cin >> cidades[i];
    cidades[0] = cidades[1], cidades[n + 1] = 1e9;
    for(int i = 1; i < n; i++){
        if(i == 1){
            soma1[i] = 1;
            continue;
        }
        if(abs(cidades[i] - cidades[i + 1]) < abs(cidades[i] - cidades[i - 1])) soma1[i] = soma1[i - 1] + 1;
        else soma1[i] = soma1[i-1] + abs(cidades[i] - cidades[i + 1]);
    }
    for(int i = n; i > 1; i--){
        if(i == n){
            soma2[i] = 1;
            continue;
        }
        if(abs(cidades[i] - cidades[i - 1]) < abs(cidades[i] - cidades[i + 1])) soma2[i] = soma2[i + 1] + 1;
        else soma2[i] = soma2[i + 1] + abs(cidades[i - 1] - cidades[i]);
    }
    int m; cin >> m;
    while(m--){
        int a, b; cin >> a >> b;
        if(a < b) cout << soma1[b - 1] - soma1[a - 1] << '\n';
        else if(a > b) cout << soma2[b + 1] - soma2[a + 1] << '\n';
        else cout << '0' << '\n';
    }
    
}
signed main(){
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);int t;cin>>t;while(t--)resolver();
}


F - Encontro de Trabalhdaores

Método de Ordenação Mesclada (Merge Sort)

A maioria das soluções usa busca binária e ponteiros duplos. Aqui apresentamos uma solução usando pares de inversão. Como cada trabalhador tem uma direção fixa e não muda, após percorrer o caminho, se um trabalhador com número menor chegar atrás de um com número maior, eles certamente se encontrarão. Podemos usar pares de inversão.

Importante:

  • As coordenadas dos trabalhadores não estão necessariamente em ordem crescente, como pode enganar nos exemplos. Devemos primeiro ordená-las.
  • Para coordenadas iguais, ordenamos por número decrescente de identificação para facilitar a contagem.
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
struct trabalhador{
    int pos, id, direcao;
    bool operator<(const trabalhador &w) const{
        if(pos == w.pos) return id > w.id;
        else return pos < w.pos;
    }
}a[N];
int temp[N], inversoes;
bool cmp(trabalhador a, trabalhador b){
    return a.pos < b.pos;
}
void merge_sort(int l, int r){
    if(l == r) return;
    int mid = l + r >> 1, i = l, j = mid + 1, num = l;
    merge_sort(l, mid), merge_sort(mid + 1, r);
    while(i <= mid && j <= r){
        if(a[i].id <= a[j].id) temp[num++] = a[i++].id;
        else inversoes += mid - i + 1, temp[num++] = a[j++].id;
    }
    while(i <= mid) temp[num++] = a[i++].id;
    while(j <= r) temp[num++] = a[j++].id;
    for(int i = l; i <= r; i++) a[i].id = temp[i];
}
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n, tempo; cin >> n >> tempo;
    string direcoes; cin >> direcoes;
    direcoes = " " + direcoes;
    for(int i = 1; i <= n; i++){
        int x; cin >> x;
        a[i] = {x, i, direcoes[i] - '0'};  
    }
    sort(a + 1, a + 1 + n, cmp);
    for(int i = 1; i <= n; i++){
        a[i].id = i;
        if(a[i].direcao == 1) a[i].pos += tempo;
        else a[i].pos -= tempo;
    }
    sort(a + 1, a + 1 + n);
    merge_sort(1, n);
    cout << inversoes << '\n';
    return 0;
}


Método de Busca Binária ou Ponteiros Duplos

Ordenamos as formigas. Sabemos que dois trabalhadores na mesma direção não podem se encontrar. Portanto, dividimos os trabalhadores em dois grupos: um indo para a esquerda e outro para a direita.

  • Para um trabalhador indo para a esquerda com identificação \(x\), para encontrar um trabalhador indo para a direita com quem ele se encontre, a diferença entre suas posições deve ser menor que \(2 * tempo\). Portanto, fazemos uma busca binária por um trabalhador indo para a direita com identificação \(y\) tal que sua posição seja menor ou igual à posição do trabalhador \(x\) + \(2 * tempo\). Nessa faixa \(x\) ~ \(y\), todos os trabalhadores se encontrarão com ele, ou seja, \(y - x\) trabalhadores.
  • Usando dois ponteiros \(l = r = 1\), enumeramos a identificação do trabalhador indo para a direita \(i\). Se o trabalhador indo para a esquerda com identificação \(l\) tem\(x_i > x_l\), incrementamos até \(x_l > x_i\). Da mesma forma para \(r\), queremos que não ultrapasse \(x_r - x_i <= 2 * tempo\). A resposta para o trabalhador atual \(i\) é \(r - l\).

G - Maximizando o Produto

Este problema parece complexo inicialmente. Embora a programação dinâmica seja uma abordagem óbvia, a equação resultante é difícil de manter:

[dp_i = dp_j + dp2_j * s(i, j) ]ou

[dp_i = min(a_i...a_j) * s(a_i...a_j) ]É difícil manter o mínimo e a soma do intervalo. Portanto, consideramos se uma abordagem gulosa é viável. Podemos enumerar o valor mínimo, o que nos dá \(O(n)\) enumerações. Definimos uma estrutura para manter informações: valor \(val\) e posição \(id\).

Considerando que a resposta composta por um valor mínimo é fixa, ou seja, expandindo para a esquerda e direita a partir do mínimo até encontrar um valor menor, esse intervalo é a contribuição máxima que podemos obter com esse mínimo. Isso está correto porque não existe um maior intervalo com esse número como mínimo.

Ordenamos em ordem crescente, marcamos cada número enumerado e paramos a expansão quando encontramos um marcado. Como marcamos \(n\) elementos, a complexidade é \(O(nlogn + n)\). Como os valores não excedem \(10^6\), podemos usar ordenação por bucket para otimizar para \(O(n + n)\).

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
struct elemento{
    int valor, posicao;
    bool operator<(const elemento &w) const{
        return valor < w.valor;
    }
}a[N];
int soma[N];
bool visitado[N];
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n; cin >> n;
    for(int i = 1; i <= n; i++){
        int x; cin >> x; 
        a[i] = {x, i};
        soma[i] = soma[i - 1] + a[i].valor;
    }
    sort(a + 1, a + 1 + n);
    int resposta = a[1].valor * soma[n];
    visitado[0] = true, visitado[a[1].posicao] = true;
    for(int i = 2; i <= n; i++){
        int l = a[i].posicao, r = a[i].posicao;
        while(l && !visitado[l - 1]) l --;
        while(r < n && !visitado[r + 1]) r ++;
        resposta = max(resposta, a[i].valor * (soma[r] - soma[l - 1]));
        visitado[a[i].posicao] = true;
    }
    cout << resposta << '\n';
    return 0;
}


H - Dividindo o Bolo

Derivando a equação, seja \(dp_i\) a contribuição máxima na posição \(i\) e \(s_i\) a soma prefixa, temos:

[dp_i = max(s_i - s_j)|j∈[i - m, i - 1] ]Notamos que \(s_i\) é independente de \(j\), então podemos fatorá-lo:

[dp_i = s_i - min(s_{j_1}, s_{j_2}, s_{j_3}....s_{j}) | j∈[i - m, i - 1] ]Portanto, precisamos manter o valor mínimo estático em um intervalo de somas prefixas, o que pode ser feito com árvore de segmentos, tabela ST, árvore de Fenwick ou fila monotônica. Aqui usamos uma fila monotônica:

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n, m; cin >> n >> m;
    vector<int> valores(n + 1), soma(n + 1);
    for(int i = 1; i <= n; i++){
        cin >> valores[i];
        soma[i] = soma[i - 1] + valores[i];
    }
    vector<int> fila(n + 1), dp(n + 1);
    int inicio = 0, fim = -1;
    for(int i = 0; i <= n; i++){
        while(inicio <= fim && fila[inicio] + m < i) inicio ++;
        while(inicio <= fim && soma[i] <= soma[fila[fim]]) fim --;
        fila[++fim] = i, dp[i] = fila[inicio];
    }
    int resultado = 0;
    for(int i = 1; i <= n; i++){
        resultado = max(resultado, soma[i] - soma[dp[i]]);
    }
    cout << resultado << '\n';
    return 0;
}


Tags: Algoritmos estruturas-de-dados programacao-competitiva Otimização matematica-computacional

Publicado em 7-9 04:46