Análise e Soluções de Competição NOIP 2019.08.09 Nível Avançado

Acordei às oito da manhã para analisar os problemas. O problema T1 parecia similar a um desafio que já havia resolvido anteriormente, envolvendo BFS com otimizações, mas o código poderia ser extenso, então decidi adiar. O problema T2 tinha restrições idênticas a um problema D de uma competição Codeforces que resolvi há um ano, embora o objetivo fosse diferente. Ainda sonolento, não quis deduzir as fórmulas e também adiei. O problema T3 parecia ser sobre propriedades interessantes, com uma solução O(n²) óbvia, mas a otimização exigiria melhorias nas consultas de intervalo e no método de verificação, o que também me desencorajou no momento.

Decidi abordar os problemas em ordem. O T1 parecia um problema de caminho mínimo. Comecei a codificar às 8:25, terminei em meia hora após alguns contratempos, fiz uma depuração rápida e deixei de lado.

Saí para o café da manhã e, no caminho, percebi que o T3 precisava verificar apenas O(n) intervalos, mas ainda só conseguia uma verificação O(n²).

De volta, comecei o T2. Tive uma dificuldade momentânea por um erro de lógica, mas o resto correu bem. Terminei e ajustei o código às 10:30.

Retornei ao T3, escrevi uma solução para 60 pontos em 10 minutos, e então encontrei uma forma de obter a pontuação máxima. Fiquei preso em um detalhe de borda por um tempo, mas finalizei às 11:20.

A plataforma de avaliação demorou a processar as submissões. Após o almoço, descobri que o T3 falhou. O erro era ter escrito sum\[r\] em vez de sum\[r+1\]. Corrigi e a solução foi aceita. Foi frustrante.

Claramente, preciso aprimorar minha habilidade de codificação.

Soluções

T1

Uma abordagem BFS direta pode não ser suficiente. A melhor estratégia é modelar como um problema de caminho mínimo. Primeiro, conecte os pontos vizinhos (quatro direções). Depois, considere o efeito dos teletransportes: um ponto pode alcançar a parede mais próxima em qualquer direção, com um custo igual ao tempo para chegar à parede mais próxima.

Construa o grafo e aplique um algoritmo de caminho mínimo.

#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
const int MAXN = 510, INF = 1e9;
int linhas, colunas;
char grade[MAXN][MAXN];
int distParede[MAXN][MAXN];
int idPonto[MAXN][MAXN], totalNos;
int origem, destino;
int adj[250010][250010];

struct Estado {
    int no, dist;
    bool operator<(const Estado& outro) const {
        return dist > outro.dist;
    }
};

void calcularDistParedes() {
    queue<pair<int,int>> fila;
    for (int i = 0; i < linhas; i++) {
        for (int j = 0; j < colunas; j++) {
            if (grade[i][j] == '#') {
                fila.push({i, j});
                distParede[i][j] = 0;
            }
        }
    }
    int dirs[4][2] = {{0,1}, {1,0}, {0,-1}, {-1,0}};
    while (!fila.empty()) {
        auto [x, y] = fila.front(); fila.pop();
        for (auto& d : dirs) {
            int nx = x + d[0], ny = y + d[1];
            if (nx >= 0 && nx < linhas && ny >= 0 && ny < colunas && 
                grade[nx][ny] == '.' && distParede[nx][ny] == -1) {
                distParede[nx][ny] = distParede[x][y] + 1;
                fila.push({nx, ny});
            }
        }
    }
}

void encontrarParedeMaisProxima() {
    for (int i = 0; i < linhas; i++) {
        for (int j = 0; j < colunas; j++) {
            if (grade[i][j] == '.') {
                if (i > 0 && grade[i-1][j] == '#') {
                    adj[idPonto[i][j]][idPonto[i-1][j]] = distParede[i][j];
                }
                // Repetir para outras direções
            }
        }
    }
}

int dijkstra() {
    vector<int> dist(totalNos, INF);
    dist[origem] = 0;
    priority_queue<Estado> pq;
    pq.push({origem, 0});
    while (!pq.empty()) {
        Estado atual = pq.top(); pq.pop();
        if (atual.dist > dist[atual.no]) continue;
        if (atual.no == destino) return atual.dist;
        for (int i = 0; i < totalNos; i++) {
            if (adj[atual.no][i]) {
                int novoDist = atual.dist + adj[atual.no][i];
                if (novoDist < dist[i]) {
                    dist[i] = novoDist;
                    pq.push({i, novoDist});
                }
            }
        }
    }
    return -1;
}

int main() {
    freopen("cell.in", "r", stdin);
    freopen("cell.out", "w", stdout);
    memset(distParede, -1, sizeof(distParede));
    cin >> linhas >> colunas;
    for (int i = 0; i < linhas; i++) {
        cin >> grade[i];
        for (int j = 0; j < colunas; j++) {
            if (grade[i][j] == '.') {
                idPonto[i][j] = totalNos++;
            }
        }
    }
    for (int i = 0; i < linhas; i++) {
        for (int j = 0; j < colunas; j++) {
            if (grade[i][j] == 'C') {
                origem = idPonto[i][j]; grade[i][j] = '.';
            } else if (grade[i][j] == 'F') {
                destino = idPonto[i][j]; grade[i][j] = '.';
            }
        }
    }
    calcularDistParedes();
    encontrarParedeMaisProxima();
    int resultado = dijkstra();
    if (resultado != -1) cout << resultado;
    else cout << "no";
    return 0;
}

T2

Aproveitando as propriedades das BSTs, a primeira etapa é ordenar os nós pelo valor da chave. Em seguida, pré-processe uma matriz para verificar se quaisquer dois nós podem ser conectados.

Para a DP, em vez de uma abordagem O(n⁵), otimize. A left e right subtrees are independentes. Defina f\[i\]\[j\]\[0\] como o valor máximo para o intervalo \[i,j\] como a subárvore direita de i-1, e f\[i\]\[j\]\[1\] como o valor máximo como a subárvore esquerda de j+1. A transição envolve escolher qual nó será o filho direto de i-1 ou o filho esquerdo de j+1. Isso resulta em uma complexidade de O(n³).

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
#define ll long long
const int MAXN = 310;
int totalNos;
ll dp[MAXN][MAXN][2];
ll prefixo[MAXN];
pair<int, int> nos[MAXN];
int conexaoPossivel[MAXN][MAXN];

ll gcd(ll a, ll b) {
    return b ? gcd(b, a % b) : a;
}

int main() {
    freopen("tree.in", "r", stdin);
    freopen("tree.out", "w", stdout);
    ios::sync_with_stdio(false);
    cin >> totalNos;
    for (int i = 1; i <= totalNos; i++) {
        cin >> nos[i].first >> nos[i].second;
    }
    sort(nos + 1, nos + 1 + totalNos);
    for (int i = 1; i <= totalNos; i++) {
        for (int j = 1; j <= totalNos; j++) {
            conexaoPossivel[i][j] = gcd(nos[i].first, nos[j].first);
        }
    }
    for (int i = 1; i <= totalNos; i++) {
        prefixo[i] = prefixo[i - 1] + nos[i].second;
    }
    memset(dp, -0x3f, sizeof(dp));
    for (int i = 1; i <= totalNos; i++) {
        if (i > 1 && conexaoPossivel[i][i-1] != 1) dp[i][i][0] = nos[i].second;
        if (i < totalNos && conexaoPossivel[i][i+1] != 1) dp[i][i][1] = nos[i].second;
    }
    ll resposta = -1;
    for (int tam = 2; tam <= totalNos; tam++) {
        for (int i = 1; i + tam - 1 <= totalNos; i++) {
            int j = i + tam - 1;
            for (int k = i; k <= j; k++) {
                ll val;
                if (k == i) val = dp[i+1][j][0] + prefixo[j] - prefixo[i-1];
                else if (k == j) val = dp[i][j-1][1] + prefixo[j] - prefixo[i-1];
                else val = dp[i][k-1][1] + dp[k+1][j][0] + prefixo[j] - prefixo[i-1];
                
                if (i > 1 && conexaoPossivel[k][i-1] != 1) dp[i][j][0] = max(dp[i][j][0], val);
                if (j < totalNos && conexaoPossivel[k][j+1] != 1) dp[i][j][1] = max(dp[i][j][1], val);
                
                if (tam == totalNos) resposta = max(resposta, val);
            }
        }
    }
    if (resposta < 0) cout << "-1";
    else cout << resposta;
    return 0;
}

T3

Para 30 pontos, uma força bruta O(n³) é suficiente: enumerar intervalos e verificar cada um. Para 60 pontos, O(n²), enumere o centro da rotação e agrupe as verificações.

Para a solução completa, note que os intervalos válidos podem ser otimizados para O(n). Se \[l,r\] é um intervalo de rotação onde a\[l\] != r e a\[r\] != l, a troca é inútil. O intervalo pode ser encolhido. Portanto, só precisamos verificar intervalos da forma \[i,a\[i\]\] ou \[a\[i\],i\].

Para otimizar a verificação, agrupe os intervalos com o mesmo centro de rotação, ordenados por comprimento. Para um intervalo \[l,r\], a resposta é pontosVálidos(1,l-1) + k + pontosVálidos(r+1,n), onde k é o número de intervalos processados. Isso funciona porque o aumento em nowsum só ocorre devido às rotações.

#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
using namespace std;
int totalElementos;
int arr[100010];
int prefixo[100010];
vector<int> grupos[200010];

int main() {
    ios::sync_with_stdio(false);
    cin >> totalElementos;
    for (int i = 1; i <= totalElementos; i++) {
        cin >> arr[i];
        prefixo[i] = prefixo[i-1] + (arr[i] == i);
    }
    for (int i = 1; i <= totalElementos; i++) {
        grupos[i + arr[i]].push_back(abs(i - arr[i]) + 1);
    }
    int resposta = prefixo[totalElementos];
    for (int soma = 1; soma <= 2 * totalElementos; soma++) {
        if (grupos[soma].empty()) continue;
        sort(grupos[soma].begin(), grupos[soma].end());
        for (size_t idx = 0; idx < grupos[soma].size(); idx++) {
            int comprimento = grupos[soma][idx];
            int esq, dir;
            if (soma % 2 == 0) {
                esq = soma / 2 - comprimento / 2;
                dir = soma / 2 + (comprimento - 1) / 2;
            } else {
                esq = soma / 2 - comprimento / 2 + 1;
                dir = soma / 2 + comprimento / 2;
            }
            resposta = max(resposta, (int)idx + 1 + prefixo[esq-1] + prefixo[totalElementos] - prefixo[dir]);
        }
    }
    cout << resposta;
    return 0;
}

Tags: NOIP Algoritmo Grafos programação dinâmica Otimização de Intervalos C++

Publicado em 9-11 14:44