Lista de exercícios

Exercícios resolvidos: força bruta e estratégia gulosa

Exercícios progressivos sobre enumeração de subconjuntos, seleção gulosa de atividades, contraexemplos, garantia e custo.

Esta lista aplica força bruta e estratégia gulosa ao problema de selecionar a maior quantidade de atividades compatíveis. Cada atividade ocupa o intervalo semiaberto [início, fim). Portanto, uma atividade pode começar exatamente quando outra termina.

Não há entrega, nota ou correção individual. Resolva um exercício por vez e consulte sua solução logo depois. Nas soluções, “Resposta esperada” significa apenas uma resposta completa para comparação e autocorreção.

Exercício 1: reconhecer objetivo e viabilidade

Considere as atividades:

A = [0, 3)
B = [3, 5)
C = [2, 4)
D = [5, 8)

Classifique cada subconjunto como viável ou inviável e informe sua quantidade de atividades:

  1. {A, B, D}
  2. {A, C, D}
  3. {B, C, D}
Solução

Explicação

Um subconjunto é viável quando nenhum par se sobrepõe. Os limites são semiabertos, então A e B são compatíveis porque A termina no instante em que B começa.

Resposta esperada

  1. {A, B, D} é viável e contém três atividades.
  2. {A, C, D} é inviável porque A e C se sobrepõem. O subconjunto contém três atividades, mas não constitui uma solução válida.
  3. {B, C, D} é inviável porque B e C se sobrepõem. O subconjunto contém três atividades, mas não constitui uma solução válida.

Exercício 2: distinguir garantia e rapidez

Classifique cada afirmação como correta ou incorreta e justifique em uma frase:

  1. A força bruta encontra uma solução ótima quando gera todos os subconjuntos e verifica corretamente a viabilidade.
  2. Toda regra local rápida constitui um algoritmo guloso ótimo.
  3. Na seleção da maior quantidade de atividades compatíveis, escolher a atividade compatível que termina mais cedo preserva uma solução ótima.
Solução

Explicação

A garantia da força bruta vem da busca completa. Uma estratégia gulosa examina menos candidatos, por isso precisa de uma justificativa específica. Para a seleção de atividades, o argumento de troca sustenta a regra do menor término.

Resposta esperada

  1. Correta. A solução ótima está entre os subconjuntos examinados e será comparada com as demais soluções viáveis.
  2. Incorreta. Uma regra local pode ser rápida e ainda bloquear uma solução global melhor.
  3. Correta. Trocar a primeira atividade de uma solução ótima pela atividade que termina mais cedo não bloqueia as atividades seguintes.

Exercício 3: interpretar máscaras de subconjuntos

Considere quatro atividades A, B, C e D. O bit de índice 0, localizado à direita, representa A. Os bits de índices 1, 2 e 3 representam B, C e D, respectivamente.

Converta cada máscara no subconjunto correspondente:

  1. 0000
  2. 0101
  3. 1010
  4. 1101
Solução

Explicação

Cada bit igual a 1 inclui sua atividade. A leitura precisa respeitar a correspondência D C B A da esquerda para a direita.

Resposta esperada

  1. 0000 corresponde a {}.
  2. 0101 corresponde a {A, C}.
  3. 1010 corresponde a {B, D}.
  4. 1101 corresponde a {A, C, D}.

Exercício 4: calcular o custo da força bruta

Considere n = 10 atividades. A força bruta gera todos os subconjuntos e pode comparar até n(n - 1) / 2 pares em cada subconjunto.

Calcule:

  1. a quantidade de subconjuntos;
  2. a quantidade máxima de pares verificados por subconjunto;
  3. o limite resultante de comparações de pares e sua ordem assintótica.
Solução

Explicação

Cada atividade possui duas decisões independentes: entrar ou não entrar. Para dez atividades, isso produz 2102^{10} subconjuntos. A verificação completa de um subconjunto considera todos os pares possíveis.

Resposta esperada

  1. 210=1.0242^{10} = 1.024 subconjuntos.
  2. 10(101)/2=4510(10 - 1) / 2 = 45 pares por subconjunto.
  3. No máximo 1.024×45=46.0801.024 \times 45 = 46.080 comparações de pares nessa contagem. Para n atividades, a ordem é O(n22n)O(n^2 2^n).

Exercício 5: implementar a compatibilidade entre duas atividades

Considere somente intervalos válidos, com start < finish, e o tipo:

typedef struct {
    const char *name;
    int start;
    int finish;
} activity;

Implemente a função abaixo. Ela deve devolver true quando as duas atividades forem compatíveis:

static bool are_compatible(const activity *first, const activity *second);
Solução

Explicação

Duas atividades são compatíveis quando a primeira termina antes do início da segunda ou quando a segunda termina antes do início da primeira. A igualdade precisa ser aceita por causa dos intervalos semiabertos.

Resposta esperada

static bool are_compatible(const activity *first, const activity *second) {
    return first->finish <= second->start || second->finish <= first->start;
}

Exercício 6: enumerar soluções por força bruta

Considere as atividades:

A = [0, 2)
B = [1, 4)
C = [2, 5)
D = [5, 7)

Enumere todos os subconjuntos viáveis e identifique uma solução ótima. Inclua o subconjunto vazio e os subconjuntos unitários.

Solução

Explicação

Existem 24=162^4 = 16 subconjuntos candidatos. Um conflito em qualquer par torna todo o subconjunto inviável. Entre os pares, A é compatível com C porque A termina em 2 e C começa em 2.

Resposta esperada

Os subconjuntos viáveis são:

{}
{A}
{B}
{C}
{D}
{A, C}
{A, D}
{B, D}
{C, D}
{A, C, D}

A solução ótima é {A, C, D}, com três atividades. Nenhum outro subconjunto viável possui três ou quatro atividades.

Exercício 7: rastrear a estratégia gulosa

As operações são cumulativas. Considere as atividades na ordem de entrada:

K = [5, 9)
L = [1, 3)
M = [3, 5)
N = [0, 7)
O = [5, 7)
P = [8, 10)

Ordene por término crescente, colocando primeiro as atividades que terminam mais cedo. Em caso de empate no término, use o início crescente: ao comparar duas atividades que terminam no mesmo instante, coloque primeiro a que começa mais cedo, ou seja, a que possui o menor início. Por exemplo, N = [0, 7) e O = [5, 7) terminam em 7. Como 0 < 5, N aparece antes de O. Esse desempate define a ordem de análise, mas não significa que ambas serão aceitas.

Depois, registre para cada atividade a decisão de aceitar ou rejeitar e o valor de last_finish após a decisão.

Solução

Explicação

A ordem é L, M, N, O, K, P. A primeira atividade é aceita. Depois disso, uma atividade somente é aceita quando start >= last_finish. Uma rejeição não altera last_finish.

Resposta esperada

L = [1, 3): aceitar, last_finish = 3
M = [3, 5): aceitar, last_finish = 5
N = [0, 7): rejeitar, last_finish = 5
O = [5, 7): aceitar, last_finish = 7
K = [5, 9): rejeitar, last_finish = 7
P = [8, 10): aceitar, last_finish = 10

O resultado é {L, M, O, P}, com quatro atividades.

Exercício 8: diagnosticar a falta de ordenação

Aplique a regra start >= last_finish diretamente à ordem de entrada do exercício anterior, sem ordenar. Depois, identifique a condição necessária que foi violada e compare o resultado com a solução do exercício 7.

Solução

Explicação

Sem ordenação, K = [5, 9) é aceita primeiro. Todas as demais atividades começam antes de 9, inclusive P, que começa em 8. O resultado continua viável, mas perde a garantia de quantidade máxima.

Resposta esperada

K = [5, 9): aceitar, last_finish = 9
L = [1, 3): rejeitar, last_finish = 9
M = [3, 5): rejeitar, last_finish = 9
N = [0, 7): rejeitar, last_finish = 9
O = [5, 7): rejeitar, last_finish = 9
P = [8, 10): rejeitar, last_finish = 9

O resultado sem ordenação é {K}, com uma atividade. A condição violada foi a ordenação prévia por término crescente. Como essa etapa foi ignorada, as atividades foram processadas na ordem de entrada, e não da que termina mais cedo para a que termina mais tarde. Com a ordenação, o exercício 7 seleciona quatro atividades.

Exercício 9: verificar uma troca segura

Considere:

G = [1, 3)
X = [0, 4)
Y = [4, 6)
Z = [6, 8)

A solução ótima {X, Y, Z} começa com X. Substitua X por G e justifique por que a quantidade e a viabilidade são preservadas.

Solução

Explicação

G termina em 3, antes de X, que termina em 4. As atividades posteriores começam em 4 ou depois. Portanto, tudo que cabia após X também cabe após G.

Resposta esperada

A troca produz {G, Y, Z}. O novo conjunto continua viável e mantém três atividades. Como finish(G) <= finish(X), a troca não bloqueia Y nem Z. Isso demonstra que existe uma solução ótima iniciada pela escolha gulosa G.

Exercício 10: refutar a regra do menor início

Um programador propôs escolher sempre a atividade compatível com menor instante de início. Aplique essa regra às atividades:

A = [0, 10)
B = [1, 3)
C = [3, 5)
D = [5, 7)
E = [7, 9)

Mostre o resultado da regra e uma solução viável melhor. Conclua se a regra possui garantia de ótimo.

Solução

Explicação

A regra escolhe A porque ela começa em 0. Essa atividade ocupa todo o período usado pelas outras atividades. Para refutar a garantia, basta apresentar uma solução viável com valor do objetivo maior.

Resposta esperada

A regra do menor início produz {A}, com uma atividade. O conjunto {B, C, D, E} é viável e contém quatro atividades. Portanto, essa entrada é um contraexemplo e a regra do menor início não garante uma solução ótima.

Exercício 11: posicionar e testar bits em C

Este exercício introduz as operações de bits usadas na implementação da força bruta. Não se pressupõe conhecimento anterior sobre esses operadores.

Definida por <stdint.h>, a macro UINT64_C(1) representa o valor 1 como um inteiro sem sinal de 64 bits. A expressão UINT64_C(1) << index desloca esse único bit 1 até a posição index. O operador & permite verificar se esse bit também está ativo em uma máscara.

Considere a máscara 0101, na qual os bits representam D C B A, da esquerda para a direita. Complete a tabela calculando o bit de cada atividade, o resultado de mask & bit e a presença da atividade no subconjunto.

AtividadeÍndicebitmask & bitPresente?
A0???
B1???
C2???
D3???
Solução

Explicação

O deslocamento posiciona um único bit para representar uma atividade. A operação mask & bit preserva esse bit somente quando ele também vale 1 na máscara. Resultado zero significa ausência; resultado diferente de zero significa presença.

Resposta esperada

AtividadeÍndicebitmask & bitPresente?
A000010001Sim
B100100000Não
C201000100Sim
D310000000Não

Portanto, a máscara 0101 representa o subconjunto {A, C}.

Exercício 12: percorrer e contar os bits de uma máscara

As operações são cumulativas. Considere mask = 1101 e count = 0.

A expressão mask & UINT64_C(1) lê o bit mais à direita. Depois da contagem, mask >>= 1U desloca a máscara uma posição para a direita e descarta o bit já processado. O sufixo U indica uma constante inteira sem sinal.

Rastreie o laço até mask se tornar 0000. Em cada repetição, registre a máscara antes da leitura, o bit lido, o valor atualizado de count e a máscara após o deslocamento.

Solução

Explicação

Cada repetição processa exatamente uma posição. O bit lido vale 0 ou 1 e pode ser somado diretamente a count. O deslocamento prepara a posição seguinte para a próxima repetição.

Resposta esperada

Repetiçãomask antesBit lidocount depoismask depois
11101110110
20110010011
30011120001
40001130000

A máscara contém três bits ativos, portanto representa um subconjunto com três atividades.

Exercício 13: implementar a seleção completa de atividades

Implemente em C um programa completo que resolva a seleção da maior quantidade de atividades compatíveis usando as duas estratégias estudadas.

  1. Na força bruta, enumere os subconjuntos por máscaras, descarte os que possuem sobreposição e preserve o maior subconjunto viável.
  2. No algoritmo guloso, ordene as atividades por término crescente e aceite cada atividade compatível com a última escolhida. Use qsort ou outro método de ordenação já conhecido.
  3. Execute as duas estratégias com as atividades do exercício 7 e apresente a quantidade e os intervalos selecionados.

Use intervalos válidos no formato [start, finish) e limite a demonstração da força bruta a 20 atividades, devido ao custo exponencial. O programa deve usar C17 e não precisa incluir uma bateria de testes: main deve demonstrar os dois métodos sobre a mesma entrada.

Solução

Explicação

Os exercícios anteriores já estabeleceram a representação dos subconjuntos, a verificação de compatibilidade, a busca completa e a escolha gulosa. A implementação integra essas operações em funções separadas e usa a mesma entrada para tornar os resultados comparáveis.

A solução de referência usa qsort, mas esse mecanismo específico não é obrigatório. Qualquer ordenação correta por término crescente preserva o método guloso estudado.

Resposta esperada

Para as atividades do exercício 7, as duas estratégias devem selecionar quatro atividades compatíveis:

Força bruta (4 atividades):
  L: [1, 3)
  M: [3, 5)
  O: [5, 7)
  P: [8, 10)

Guloso (4 atividades):
  L: [1, 3)
  M: [3, 5)
  O: [5, 7)
  P: [8, 10)

Compare a implementação com a solução completa em C.