Diário de Competição NOIP 2023

Reflexões sobre o Desempenho

Dia -1: Revisei estruturas de dados como Tabelas de Segmentação (ST), Árvores de Segmento (Segment Tree), KMP e LCA. Infelizmente, nenhum desses tópicos apareceu na prova.

Dia 0: Uma nova revisão geral, sentindo uma mistura de confiança e incerteza. Planejei a estratégia para o dia da prova e depois descansei.

Dia 1:

  • Acordei às 6:30 e fiz uma última revisão.
  • Após o café da manhã, dirigi-me ao local da prova.
  • Cheguei às 7:30, o local estava deserto. Logo depois, o professor Z chegou, me desejou sorte e me acompanhou até a sala de exames.
  • Às 8:00, entrei na sala e encontrei o colega @hzlqwq. Após uma breve admiração, ele humildemente comentou que só conseguiria resolver o primeiro problema.

Problema 1: O Dicionário Misterioso

  • A prova começou às 8:30.
  • Às 8:31, o primeiro problema parecia simples. Comecei a codificar imediatamente.
  • Às 9:30, percebi que meu código não passava em alguns casos de teste com datas. Uma peculiaridade inesperada. Tentei depurar com os exemplos fornecidos.
  • Às 10:00, ainda confuso, verifiquei meu código e o comparei com a lógica esperada.
  • Às 10:10, relendo o enunciado, descobri meu erro: interpretei w'\_i como w\_i. Um lapso crucial. A solução exigia apenas manter o valor máximo.
  • Às 10:15, com a correção implementada, o exemplo principal passou. No entanto, demorou 4 segundos. Precisava otimizar o código. Após alguns ajustes, o tempo caiu para 3 segundos. Confiando que o hardware do CCF (i7-8 com O2) seria mais rápido que meu i3-8 local, esperei o resultado.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cstdio>
#include <cstring>

struct Entry {
    std::string sorted_chars;
    int original_index;
};

bool compareEntries(const Entry& a, const Entry& b) {
    return a.sorted_chars < b.sorted_chars;
}

int main() {
    freopen("dict.in", "r", stdin);
    freopen("dict.out", "w", stdout);

    int n, m;
    scanf("%d %d", &n, &m);

    std::vector<std::string> words(n);
    for (int i = 0; i < n; ++i) {
        char buffer[100]; // Buffer para ler a string
        scanf("%s", buffer);
        words[i] = buffer;
    }

    if (n == 1) {
        std::cout << 1 << std::endl;
        return 0;
    }

    std::vector<Entry> entries(n);
    std::vector<std::string> sorted_word_forms(n);

    for (int i = 0; i < n; ++i) {
        int counts[26] = {0};
        for (char c : words[i]) {
            counts[c - 'a']++;
        }

        std::string lexical_order, reverse_lexical_order;
        for (int j = 0; j < 26; ++j) {
            while (counts[j]--) {
                lexical_order += (char)('a' + j);
            }
        }
        
        // Recalcula contagem para a segunda ordenação
        for (char c : words[i]) {
            counts[c - 'a']++;
        }
        for (int j = 25; j >= 0; --j) {
             while (counts[j]--) {
                reverse_lexical_order += (char)('a' + j);
            }
        }

        entries[i].sorted_chars = reverse_lexical_order; // Usar a ordem reversa para a comparação principal
        entries[i].original_index = i;
        sorted_word_forms[i] = lexical_order; // Armazenar a ordem lexical normal
    }

    std::sort(entries.begin(), entries.end(), compareEntries);

    std::vector<int> result_flags(n, 0);
    for (int i = 0; i < n; ++i) {
        // A condição de verificação parece ser complexa e específica do problema original.
        // Esta é uma interpretação baseada na estrutura do código fornecido.
        // A lógica exata de 'check' precisa ser cuidadosamente revisada com o enunciado original.
         if (sorted_word_forms[entries[i].original_index] < sorted_word_forms[entries[0].original_index] || 
             (i > 0 && sorted_word_forms[entries[i].original_index] < sorted_word_forms[entries[1].original_index])) {
             result_flags[entries[i].original_index] = 1;
         }
    }

    for (int i = 0; i < n; ++i) {
        printf("%d", result_flags[i]);
    }
    printf("\n");

    return 0;
}

Problema 2: O Mundo das Tri-Boleans

  • Às 10:40, tentei pensar em uma solução ótima. Anotei "Union-Find" no papel (mais tarde descobri que era a solução coreta, um lembrete para o futuro).
  • Contudo, não consegui implementar. Tentei uma abordagem linear, mas sem sucesso. Considreei algoritmos gulosos e DP, mas ambos se mostraram inviáveis. Decidi implementar uma solução de força bruta.
  • Às 10:50, escrevi uma função DFS para gerar todas as permutações, garantindo 20 pontos.
  • Às 11:15, adaptei o código para casos de teste especiais, obtendo mais 20 pontos.
  • Às 11:20, tentei abordar os próximos 20 pontos, mas minha hipótese inicial se mostrou incorreta.
  • Às 11:40, sem ideias adicionais, deixei os problemas restantes e passei para os próximos.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cstdio>
#include <cstring>

struct Operation {
    char op_type;
    int var_idx;
    int other_var_idx; // Usado para operadores binários
};

int N, M;
const int MAX_VARS = 100005;
Operation ops[MAX_VARS];
int current_assignment[15]; // Supondo um número pequeno de variáveis para a DFS
int evaluated_assignment[MAX_VARS];
int min_unknowns = 1e9 + 7;

void evaluate_state() {
    for (int i = 1; i <= N; ++i) {
        evaluated_assignment[i] = current_assignment[i];
    }

    for (int i = 1; i <= M; ++i) {
        char op = ops[i].op_type;
        int x = ops[i].var_idx;
        int y = ops[i].other_var_idx;

        if (op == 'T') { // TRUE
            evaluated_assignment[x] = 1; // 1: TRUE
        } else if (op == 'F') { // FALSE
            evaluated_assignment[x] = 2; // 2: FALSE
        } else if (op == 'U') { // UNKNOWN
            evaluated_assignment[x] = 3; // 3: UNKNOWN
        } else if (op == '-') { // NOT
            if (evaluated_assignment[y] == 3) {
                evaluated_assignment[x] = 3;
            } else if (evaluated_assignment[y] == 2) { // NOT FALSE = TRUE
                evaluated_assignment[x] = 1;
            } else if (evaluated_assignment[y] == 1) { // NOT TRUE = FALSE
                evaluated_assignment[x] = 2;
            }
        } else if (op == '+') { // OR (simplificado, pode precisar de lógica mais complexa)
            if (evaluated_assignment[y] == 3) {
                evaluated_assignment[x] = 3;
            } else if (evaluated_assignment[y] == 2) { // x OR FALSE = x
                evaluated_assignment[x] = evaluated_assignment[x]; 
            } else if (evaluated_assignment[y] == 1) { // x OR TRUE = TRUE
                evaluated_assignment[x] = 1;
            }
             // Se op for AND, a lógica seria diferente. Assumindo OR baseado no '+'
        }
        // Implementar lógica para outros operadores se necessário
    }

    bool consistent = true;
    for (int i = 1; i <= N; ++i) {
        if (evaluated_assignment[i] != current_assignment[i] && current_assignment[i] != 3) {
            consistent = false;
            break;
        }
    }

    if (consistent) {
        int unknown_count = 0;
        for (int i = 1; i <= N; ++i) {
            if (evaluated_assignment[i] == 3) {
                unknown_count++;
            }
        }
        min_unknowns = std::min(min_unknowns, unknown_count);
    }
}

void solve_recursive(int var_idx) {
    if (var_idx == N + 1) {
        evaluate_state();
        return;
    }

    // Tentar todas as 3 atribuições (TRUE, FALSE, UNKNOWN)
    for (int assignment_val = 1; assignment_val <= 3; ++assignment_val) {
        current_assignment[var_idx] = assignment_val;
        solve_recursive(var_idx + 1);
    }
}

// Solução simplificada para casos de teste específicos
void solve_case_specific() {
    int res2 = 0;
    char simple_assignments[MAX_VARS];
    memset(simple_assignments, ' ', sizeof(simple_assignments));

    for (int i = 1; i <= M; ++i) {
        simple_assignments[ops[i].var_idx] = ops[i].op_type;
    }
    for (int i = 1; i <= N; ++i) {
        if (simple_assignments[i] == 'U') {
            res2++;
        }
    }
     // A lógica de 'c >= 3 && c <= 4' pode precisar de ajuste fino.
     // Aqui, apenas conta 'U' que parece ser uma heurística para alguns casos.
    printf("%d\n", res2); 
}


int main() {
    freopen("tribool.in", "r", stdin);
    freopen("tribool.out", "w", stdout);

    int case_code, test_id;
    scanf("%d %d", &case_code, &test_id);

    while (test_id--) {
        min_unknowns = 1e9 + 7;
        scanf("%d %d", &N, &M);

        for (int i = 1; i <= M; ++i) {
            char op_buffer[2];
            scanf("%s", op_buffer);
            ops[i].op_type = op_buffer[0];
            scanf("%d", &ops[i].var_idx);
            if (ops[i].op_type == '-' || ops[i].op_type == '+') {
                scanf("%d", &ops[i].other_var_idx);
            } else {
                ops[i].other_var_idx = -1; // Não aplicável
            }
        }

        // A divisão de casos 'c <= 2', 'c >= 3 && c <= 4' etc. é mantida.
        if (case_code <= 2) {
             // Assumindo que N é pequeno o suficiente para DFS
             if (N <= 15) { // Limite para evitar TLE
                solve_recursive(1);
                printf("%d\n", min_unknowns);
             } else {
                 // Implementar outra lógica ou notificar que N é muito grande para DFS
                  printf("N/A\n"); // Placeholder
             }
        } else if (case_code >= 3 && case_code <= 4) {
            solve_case_specific();
        } else {
             // Caso padrão ou fallback
             if (N <= 15) {
                solve_recursive(1);
                printf("%d\n", min_unknowns);
             } else {
                 printf("N/A\n"); // Placeholder
             }
        }
    }
    return 0;
}

Problema 3: Expansão de Grades

  • Às 12:00, terminei de ler o enunciado. Parecia complexo.
  • Às 12:01, notei um caso trivial: n=1 e m=1. Implementei uma solução simples para este cenário e segui em frente.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cstdio>

int main() {
    freopen("expand.in", "r", stdin);
    freopen("expand.out", "w", stdout);

    int scenario_code, grid_rows, grid_cols, num_queries;
    scanf("%d %d %d %d", &scenario_code, &grid_rows, &grid_cols, &num_queries);

    // Caso especial onde a grade é 1x1
    if (grid_rows == 1 && grid_cols == 1) {
        int initial_x, initial_y;
        scanf("%d %d", &initial_x, &initial_y);

        while (num_queries--) {
            int kx_ops, ky_ops;
            scanf("%d %d", &kx_ops, &ky_ops);

            // Processar operações de linha (x)
            for (int i = 0; i < kx_ops; ++i) {
                int param_id, param_val;
                scanf("%d %d", &param_id, &param_val);
                if (param_id == 1) { // Atualiza valor de x
                    initial_x = param_val;
                }
            }
            // Processar operações de coluna (y)
            for (int i = 0; i < ky_ops; ++i) {
                int param_id, param_val;
                scanf("%d %d", &param_id, &param_val);
                if (param_id == 1) { // Atualiza valor de y
                    initial_y = param_val;
                }
            }

            // Saída baseada na comparação final de x e y
            if (initial_x != initial_y) {
                printf("1");
            } else {
                printf("0");
            }
        }
        printf("\n");
    } else {
        // Lógica para casos gerais (não implementada aqui)
         // A solução original não implementava os casos gerais.
         // Seria necessário adicionar código aqui para N>1 ou M>1.
    }

    return 0;
}

Problema 4: Corredores de Energia

  • Às 12:15, o enunciado deste problema foi mais amigável. Comecei com uma abordagem de enumeração de estados, garatnindo 8 pontos.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cstdio>
#include <cstring>

#define ll long long
const int MAX_N_NODES = 100005;

struct Segment {
    int start_node;
    int end_node;
    int energy_value;
};

int main() {
    freopen("run.in", "r", stdin);
    freopen("run.out", "w", stdout);

    int case_code, num_tests;
    scanf("%d %d", &case_code, &num_tests);

    while (num_tests--) {
        int n_nodes, m_segments, k_max_consecutive, d_penalty;
        scanf("%d %d %d %d", &n_nodes, &m_segments, &k_max_consecutive, &d_penalty);

        std::vector<Segment> segments(m_segments);
        for (int i = 0; i < m_segments; ++i) {
            scanf("%d %d %d", &segments[i].end_node, &segments[i].start_node, &segments[i].energy_value);
            // Ajuste os índices para serem mais intuitivos para o cálculo
            // O código original faz 'x = start - end + 1', o que pode ser confuso.
            // Vamos manter a estrutura original, mas com nomes mais claros.
            int original_start = segments[i].start_node;
            int original_end = segments[i].end_node;
            // Reinterpretando a lógica do código original:
            // x = original_end - original_start + 1 (comprimento do segmento)
            // y = original_end (nó final)
            segments[i].start_node = original_end - original_start + 1; // Comprimento
            segments[i].end_node = original_end; // Nó final
        }

        // Caso específico: enumeração de estados para N pequeno
        if (case_code <= 2) {
            ll max_total_energy = -1e18; // Inicializa com um valor muito baixo

            // Itera sobre todas as sub-máscaras de bits de 1 a N
            // (1 << (n_nodes + 1)) é o limite superior para incluir todas as combinações
            for (int state_mask = 0; state_mask < (1 << (n_nodes + 1)); ++state_mask) {
                
                // Verifica se a máscara de estado é válida (excluindo o bit 0 se não for usado)
                if (n_nodes > 0 && (state_mask & (1 << 0))) continue; // Ignora se o bit 0 está ativo e não é usado

                std::vector<bool> is_active(n_nodes + 1, false);
                int current_consecutive = 0;
                int max_consecutive_in_state = 0;

                for (int i = 1; i <= n_nodes; ++i) {
                    if ((state_mask >> i) & 1) { // Se o i-ésimo bit está ativo
                        is_active[i] = true;
                        current_consecutive++;
                    } else {
                        max_consecutive_in_state = std::max(max_consecutive_in_state, current_consecutive);
                        current_consecutive = 0;
                    }
                }
                max_consecutive_in_state = std::max(max_consecutive_in_state, current_consecutive);

                if (max_consecutive_in_state > k_max_consecutive) {
                    continue; // Ignora estados inválidos
                }

                // Calcula a energia total para este estado
                std::vector<int> active_prefix_sum(n_nodes + 1, 0);
                for (int i = 1; i <= n_nodes; ++i) {
                    active_prefix_sum[i] = active_prefix_sum[i - 1] + (is_active[i] ? 1 : 0);
                }

                ll current_energy = -(ll)active_prefix_sum[n_nodes] * d_penalty; // Penalidade inicial

                for (const auto& seg : segments) {
                    int segment_length = seg.start_node; // Comprimento do segmento
                    int end_node = seg.end_node;       // Nó final
                    int value = seg.energy_value;

                    // Verifica se todo o segmento está ativo no estado atual
                    // A condição original era: q[a[i].y] - q[a[i].x - 1] == a[i].y - a[i].x + 1
                    // Traduzindo: prefix_sum[end_node] - prefix_sum[start_node - 1] == length
                    // onde start_node = end_node - length + 1
                    int start_node = end_node - segment_length + 1;
                    if (start_node >= 1 && (active_prefix_sum[end_node] - active_prefix_sum[start_node - 1]) == segment_length) {
                        current_energy += value;
                    }
                }
                max_total_energy = std::max(max_total_energy, current_energy);
            }
             // Se nenhum estado válido foi encontrado, pode ser necessário um tratamento especial
             if (max_total_energy == -1e18) {
                  // Calcula a energia apenas com a penalidade, se nenhum segmento puder ser ativado
                  max_total_energy = -(ll)std::min(n_nodes, k_max_consecutive) * d_penalty; 
                  // Ou um valor padrão dependendo do problema
             }
            printf("%lld\n", max_total_energy);
        }
    }
    return 0;
}

Resumo Final: Falta de atenção aos detalhes e má gestão do tempo foram os principais fatores. Na próxima vez, focarei em ler os enunciados com mais cuidado e gerenciar o tempo de forma mais eficaz.

Estimativa de Pontuação:

  • Pontuação Prevista: 100 + 40 + 0 + 0 ~ 100 + 40 + 5 + 8
  • Plataforma "Xiao TuLing": 90 + 40 + 0 + 0 = 130
  • Plataforma "Luogu": 90 + 40 + 0 + 0 = 130
  • Plataforma "Yundou": 90 + 40 + 0 + 0 = 130
  • Equipe "Xinyou": 90 + 40 + 0 + 0 = 130 Todos os resultados convergiram para 130 pontos, o que é peculiar.

Observação Crítica sobre o Problema 1: Minha solução original exigia encontrar o maior e o segundo maior elemento. Em vez disso, apliquei uma ordenação completa, transformando um algoritmo $O(2 \times n \times m)$ em $O(n \times m \times \log n)$. Minha preocupação em otimizar o tempo de execução em vez de corrigir a lógica fundamental foi um erro estratégico significativo.

Tags: Programação Competitiva NOIP Algoritmos estrutura de dados C++

Publicado em 7-24 09:06