2023 Day 21 Complete
This commit is contained in:
parent
b393c79247
commit
3d42462d4c
@ -1,194 +1,85 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/list"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
h "git.bullercodeworks.com/brian/adventofcode/helpers"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
inp := h.StdinToStringSlice()
|
data, _ := os.ReadFile("input")
|
||||||
part1(inp)
|
answer := solution(data, 64)
|
||||||
fmt.Println()
|
|
||||||
part2(inp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func part1(input []string) {
|
|
||||||
m := h.StringSliceToCoordByteMap(input)
|
|
||||||
start, _ := m.FindFirst('S')
|
|
||||||
m.Put(start, '.')
|
|
||||||
result := []h.Coordinate{start}
|
|
||||||
for i := 0; i < 64; i++ {
|
|
||||||
result = findNext(&m, result)
|
|
||||||
}
|
|
||||||
fmt.Println("# Part 1")
|
fmt.Println("# Part 1")
|
||||||
fmt.Println(len(result))
|
fmt.Println(answer[0])
|
||||||
}
|
|
||||||
|
|
||||||
// 253370351984188 is too low
|
|
||||||
func part2(input []string) {
|
|
||||||
m := h.StringSliceToCoordByteMap(input)
|
|
||||||
start, _ := m.FindFirst('S')
|
|
||||||
m.Put(start, '.')
|
|
||||||
steps := 26501365
|
|
||||||
p := make([]int, 3)
|
|
||||||
size := len(input)
|
|
||||||
half := size / 2
|
|
||||||
fmt.Println("Getting Plots: p[0]")
|
|
||||||
p[0], _ = getPlots(&m, half, 5)
|
|
||||||
fmt.Println("Getting Plots: p[1]")
|
|
||||||
m = h.StringSliceToCoordByteMap(input)
|
|
||||||
p[1], _ = getPlots(&m, half+size, 5)
|
|
||||||
fmt.Println("Getting Plots: p[2]")
|
|
||||||
m = h.StringSliceToCoordByteMap(input)
|
|
||||||
p[2], _ = getPlots(&m, half+2*size, 5)
|
|
||||||
a := (p[2] + p[0] - 2*p[1]) / 2
|
|
||||||
b := p[1] - p[0] - a
|
|
||||||
c := p[0]
|
|
||||||
n := steps / size
|
|
||||||
result := a*n*n + b*n + c
|
|
||||||
|
|
||||||
fmt.Println("# Part 2")
|
fmt.Println("# Part 2")
|
||||||
fmt.Println(result)
|
fmt.Println(answer[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
var visited map[string]bool
|
func solution(data []byte, total_steps int) []int {
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||||
func getPlots(m *h.CoordByteMap, steps int, factor int) (int, error) {
|
q := list.New()
|
||||||
sx, sy := m.ColCount(), m.RowCount()
|
symbols := map[image.Point]string{}
|
||||||
l := sy
|
for y, line := range lines {
|
||||||
expandMap(m, factor)
|
l := strings.TrimSpace(line)
|
||||||
sx = l * (factor / 2)
|
//fmt.Println(len(s))
|
||||||
sy = l * (factor / 2)
|
for x, r := range l {
|
||||||
visited = make(map[string]bool)
|
symbols[image.Point{x, y}] = string(r)
|
||||||
return len(nextStep(sx, sy, 0, steps, m)), nil
|
if r == 'S' {
|
||||||
}
|
q.PushBack(image.Point{x, y})
|
||||||
|
|
||||||
func isPlot(m *h.CoordByteMap, x, y int) bool {
|
|
||||||
return m.Get(h.Coordinate{X: x, Y: y}) == '.'
|
|
||||||
}
|
|
||||||
func nextStep(x, y, step, max int, m *h.CoordByteMap) []h.Coordinate {
|
|
||||||
var acc []h.Coordinate
|
|
||||||
var key = fmt.Sprintf("%d;%d;%d", x, y, step)
|
|
||||||
if _, ok := visited[key]; ok {
|
|
||||||
return []h.Coordinate{}
|
|
||||||
} else {
|
|
||||||
visited[key] = true
|
|
||||||
}
|
|
||||||
if step == max {
|
|
||||||
return []h.Coordinate{{X: x, Y: y}}
|
|
||||||
}
|
|
||||||
if x > 0 && isPlot(m, x-1, y) {
|
|
||||||
acc = append(acc, nextStep(x-1, y, step+1, max, m)...)
|
|
||||||
}
|
|
||||||
if x < m.ColCount() && isPlot(m, x+1, y) {
|
|
||||||
acc = append(acc, nextStep(x, y-1, step+1, max, m)...)
|
|
||||||
}
|
|
||||||
if y > 0 && isPlot(m, x, y-1) {
|
|
||||||
acc = append(acc, nextStep(x, y-1, step+1, max, m)...)
|
|
||||||
}
|
|
||||||
if y < m.RowCount() && isPlot(m, x, y+1) {
|
|
||||||
acc = append(acc, nextStep(x, y+1, step+1, max, m)...)
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
|
|
||||||
func expandMap(m *h.CoordByteMap, factor int) {
|
|
||||||
stx, edx := m.TLX, m.BRX
|
|
||||||
for f := 1; f < factor; f++ {
|
|
||||||
for x := stx; x <= edx; x++ {
|
|
||||||
col := m.GetCol(x)
|
|
||||||
m.AddCol(col)
|
|
||||||
}
|
|
||||||
sty, edy := m.TLY, m.BRY
|
|
||||||
for y := sty; y <= edy; y++ {
|
|
||||||
row := m.GetRow(y)
|
|
||||||
m.AddRow(row)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func part2BruteForce(input []string) {
|
steps := 0
|
||||||
// Brute force just isn't going to cut it...
|
total := 26501365
|
||||||
m := h.StringSliceToCoordByteMap(input)
|
part_1 := 0
|
||||||
m.StringEmptyIsSpace = true
|
part_2 := 0
|
||||||
m.StringEmptyByte = '.'
|
|
||||||
seen := make(map[h.Coordinate]bool)
|
directions := []image.Point{
|
||||||
start, _ := m.FindFirst('S')
|
{-1, 0}, {0, -1}, {0, 1}, {1, 0},
|
||||||
m.Put(start, '.')
|
}
|
||||||
result := []h.Coordinate{start}
|
|
||||||
steps := 26501365
|
polynomial := make([]int, 0)
|
||||||
for i := 0; i < steps; i++ {
|
for steps < total {
|
||||||
fmt.Println(h.CLEAR_SCREEN)
|
new_Q := list.New()
|
||||||
fmt.Printf("%d/%d (%f)\n", i, steps, float64(i)/float64(steps))
|
visited := make(map[image.Point]bool)
|
||||||
seen, result = findNextInfiniteRepeat(&m, seen, result)
|
|
||||||
if i == 100 {
|
for q.Len() > 0 {
|
||||||
|
element := q.Front()
|
||||||
|
val := element.Value.(image.Point)
|
||||||
|
q.Remove(element)
|
||||||
|
x, y := val.X, val.Y
|
||||||
|
for _, d := range directions {
|
||||||
|
newPos := image.Point{x + d.X, y + d.Y}
|
||||||
|
ref := image.Point{((newPos.Y % len(lines)) + len(lines)) % len(lines), ((newPos.X % len(lines)) + len(lines)) % len(lines)}
|
||||||
|
if symbols[ref] != "#" {
|
||||||
|
if _, ok := visited[newPos]; !ok {
|
||||||
|
visited[newPos] = true
|
||||||
|
new_Q.PushBack(newPos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps += 1
|
||||||
|
q = new_Q //update queue
|
||||||
|
if steps%(len(lines)) == total%len(lines) {
|
||||||
|
polynomial = append(polynomial, len(visited))
|
||||||
|
if len(polynomial) == 3 {
|
||||||
|
p0 := polynomial[0]
|
||||||
|
p1 := polynomial[1] - polynomial[0]
|
||||||
|
p2 := polynomial[2] - polynomial[1]
|
||||||
|
|
||||||
|
part_2 = p0 + (p1 * (total / len(lines))) + ((total/len(lines))*((total/len(lines))-1)/2)*(p2-p1)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Println("# Part 2")
|
if steps == total_steps {
|
||||||
fmt.Println(len(result))
|
part_1 = len(visited)
|
||||||
for i := range result {
|
|
||||||
m.Put(result[i], 'O')
|
|
||||||
}
|
}
|
||||||
fmt.Println(m)
|
|
||||||
// It's making a diamond shape...
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func findNext(m *h.CoordByteMap, curr []h.Coordinate) []h.Coordinate {
|
return []int{part_1, part_2}
|
||||||
var result []h.Coordinate
|
|
||||||
for _, c := range curr {
|
|
||||||
for _, tst := range []h.Coordinate{
|
|
||||||
c.North(), c.East(), c.South(), c.West(),
|
|
||||||
} {
|
|
||||||
if m.ContainsCoord(tst) && m.Get(tst) == '.' {
|
|
||||||
var contains bool
|
|
||||||
for i := range result {
|
|
||||||
if result[i].Equals(tst) {
|
|
||||||
contains = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !contains {
|
|
||||||
result = append(result, tst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func findNextInfiniteRepeat(m *h.CoordByteMap, seen map[h.Coordinate]bool, curr []h.Coordinate) (map[h.Coordinate]bool, []h.Coordinate) {
|
|
||||||
var result []h.Coordinate
|
|
||||||
for _, c := range curr {
|
|
||||||
for _, tst := range []h.Coordinate{
|
|
||||||
c.North(), c.East(), c.South(), c.West(),
|
|
||||||
} {
|
|
||||||
if seen[tst] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
wrkX, wrkY := tst.X, tst.Y
|
|
||||||
for wrkX < m.TLX {
|
|
||||||
wrkX += m.BRX
|
|
||||||
}
|
|
||||||
for wrkY < m.TLY {
|
|
||||||
wrkY += m.BRY
|
|
||||||
}
|
|
||||||
wrk := h.Coordinate{X: (wrkX % m.BRX), Y: (wrkY % m.BRY)}
|
|
||||||
if m.Get(wrk) == '.' {
|
|
||||||
var contains bool
|
|
||||||
for i := range result {
|
|
||||||
if result[i].Equals(tst) {
|
|
||||||
contains = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !contains {
|
|
||||||
result = append(result, tst)
|
|
||||||
seen[tst] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return seen, result
|
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,10 @@ type CoordByteMap struct {
|
|||||||
// Options for the 'String' method
|
// Options for the 'String' method
|
||||||
StringEmptyIsSpace bool
|
StringEmptyIsSpace bool
|
||||||
StringEmptyByte byte
|
StringEmptyByte byte
|
||||||
|
|
||||||
|
// If 'Repeats' is true, then any coordinate request will be
|
||||||
|
// modded by BRX/BRY
|
||||||
|
Repeats bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCoordByteMap() CoordByteMap {
|
func NewCoordByteMap() CoordByteMap {
|
||||||
@ -49,6 +53,9 @@ func (m *CoordByteMap) ColCount() int {
|
|||||||
return m.BRX - m.TLX
|
return m.BRX - m.TLX
|
||||||
}
|
}
|
||||||
func (m *CoordByteMap) GetRow(y int) []byte {
|
func (m *CoordByteMap) GetRow(y int) []byte {
|
||||||
|
if m.Repeats {
|
||||||
|
y = (y + m.BRY) % m.BRY
|
||||||
|
}
|
||||||
var resp []byte
|
var resp []byte
|
||||||
for x := m.TLX; x <= m.BRX; x++ {
|
for x := m.TLX; x <= m.BRX; x++ {
|
||||||
resp = append(resp, m.Get(Coordinate{X: x, Y: y}))
|
resp = append(resp, m.Get(Coordinate{X: x, Y: y}))
|
||||||
@ -56,6 +63,9 @@ func (m *CoordByteMap) GetRow(y int) []byte {
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
func (m *CoordByteMap) GetCol(x int) []byte {
|
func (m *CoordByteMap) GetCol(x int) []byte {
|
||||||
|
if m.Repeats {
|
||||||
|
x = (x + m.BRX) % m.BRX
|
||||||
|
}
|
||||||
var resp []byte
|
var resp []byte
|
||||||
for y := m.TLY; y <= m.BRY; y++ {
|
for y := m.TLY; y <= m.BRY; y++ {
|
||||||
resp = append(resp, m.Get(Coordinate{X: x, Y: y}))
|
resp = append(resp, m.Get(Coordinate{X: x, Y: y}))
|
||||||
@ -76,10 +86,18 @@ func (m *CoordByteMap) AddCol(col []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *CoordByteMap) GetPos(x, y int) byte {
|
func (m *CoordByteMap) GetPos(x, y int) byte {
|
||||||
|
if m.Repeats {
|
||||||
|
x = (x + m.BRX) % m.BRX
|
||||||
|
y = (y + m.BRY) % m.BRY
|
||||||
|
}
|
||||||
return m.Get(Coordinate{X: x, Y: y})
|
return m.Get(Coordinate{X: x, Y: y})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CoordByteMap) Get(pos Coordinate) byte {
|
func (m *CoordByteMap) Get(pos Coordinate) byte {
|
||||||
|
if m.Repeats {
|
||||||
|
pos.X = (pos.X + m.BRX) % m.BRX
|
||||||
|
pos.Y = (pos.Y + m.BRY) % m.BRY
|
||||||
|
}
|
||||||
if v, ok := m.Field[pos]; ok {
|
if v, ok := m.Field[pos]; ok {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
@ -87,6 +105,10 @@ func (m *CoordByteMap) Get(pos Coordinate) byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *CoordByteMap) Opt(pos Coordinate, def byte) byte {
|
func (m *CoordByteMap) Opt(pos Coordinate, def byte) byte {
|
||||||
|
if m.Repeats {
|
||||||
|
pos.X = (pos.X + m.BRX) % m.BRX
|
||||||
|
pos.Y = (pos.Y + m.BRY) % m.BRY
|
||||||
|
}
|
||||||
if v, ok := m.Field[pos]; ok {
|
if v, ok := m.Field[pos]; ok {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
@ -110,6 +132,10 @@ func (m *CoordByteMap) Put(pos Coordinate, val byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *CoordByteMap) Delete(pos Coordinate) {
|
func (m *CoordByteMap) Delete(pos Coordinate) {
|
||||||
|
if m.Repeats {
|
||||||
|
pos.X = (pos.X + m.BRX) % m.BRX
|
||||||
|
pos.Y = (pos.Y + m.BRY) % m.BRY
|
||||||
|
}
|
||||||
delete(m.Field, pos)
|
delete(m.Field, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +161,10 @@ func (m *CoordByteMap) ToByteSlices() [][]byte {
|
|||||||
// ContainsCoord returns true if the passed coordinate is within the bounds
|
// ContainsCoord returns true if the passed coordinate is within the bounds
|
||||||
// of the map
|
// of the map
|
||||||
func (m *CoordByteMap) ContainsCoord(c Coordinate) bool {
|
func (m *CoordByteMap) ContainsCoord(c Coordinate) bool {
|
||||||
|
if m.Repeats {
|
||||||
|
c.X = (c.X + m.BRX) % m.BRX
|
||||||
|
c.Y = (c.Y + m.BRY) % m.BRY
|
||||||
|
}
|
||||||
return c.X >= m.TLX && c.X <= m.BRX &&
|
return c.X >= m.TLX && c.X <= m.BRX &&
|
||||||
c.Y >= m.TLY && c.Y <= m.BRY
|
c.Y >= m.TLY && c.Y <= m.BRY
|
||||||
}
|
}
|
||||||
|
62
helpers/priorityQueue.go
Normal file
62
helpers/priorityQueue.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package aoc
|
||||||
|
|
||||||
|
type PriorityQueue[T comparable] struct {
|
||||||
|
items []T
|
||||||
|
comparator func(a, b T) int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPriorityQueue[T comparable](comparator func(a, b T) int) PriorityQueue[T] {
|
||||||
|
return PriorityQueue[T]{comparator: comparator}
|
||||||
|
}
|
||||||
|
func (pq *PriorityQueue[T]) Push(item T) {
|
||||||
|
pq.items = append(pq.items, item)
|
||||||
|
pq.heapifyUp(len(pq.items) - 1)
|
||||||
|
}
|
||||||
|
func (pq *PriorityQueue[T]) Pop() T {
|
||||||
|
top := pq.items[0]
|
||||||
|
lastIndex := len(pq.items) - 1
|
||||||
|
pq.items[0], pq.items[lastIndex] = pq.items[lastIndex], pq.items[0]
|
||||||
|
pq.items = pq.items[:lastIndex]
|
||||||
|
pq.heapifyDown(0)
|
||||||
|
return top
|
||||||
|
}
|
||||||
|
func (pq *PriorityQueue[T]) Peek() T {
|
||||||
|
return pq.items[0]
|
||||||
|
}
|
||||||
|
func (pq *PriorityQueue[T]) Len() int {
|
||||||
|
return len(pq.items)
|
||||||
|
}
|
||||||
|
func (pq *PriorityQueue[T]) IsEmpty() bool {
|
||||||
|
return len(pq.items) == 0
|
||||||
|
}
|
||||||
|
func (pq *PriorityQueue[T]) IsNotEmpty() bool {
|
||||||
|
return !pq.IsEmpty()
|
||||||
|
}
|
||||||
|
func (pq *PriorityQueue[T]) heapifyUp(index int) {
|
||||||
|
for index > 0 {
|
||||||
|
parentIndex := (index - 1) / 2
|
||||||
|
if pq.comparator(pq.items[index], pq.items[parentIndex]) < 0 {
|
||||||
|
pq.items[index], pq.items[parentIndex] = pq.items[parentIndex], pq.items[index]
|
||||||
|
index = parentIndex
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (pq *PriorityQueue[T]) heapifyDown(index int) {
|
||||||
|
for {
|
||||||
|
leftChild, rightChild, smallest := 2*index+1, 2*index+2, index
|
||||||
|
if leftChild < len(pq.items) && pq.comparator(pq.items[leftChild], pq.items[smallest]) < 0 {
|
||||||
|
smallest = leftChild
|
||||||
|
}
|
||||||
|
if rightChild < len(pq.items) && pq.comparator(pq.items[rightChild], pq.items[smallest]) < 0 {
|
||||||
|
smallest = rightChild
|
||||||
|
}
|
||||||
|
if smallest != index {
|
||||||
|
pq.items[index], pq.items[smallest] = pq.items[smallest], pq.items[index]
|
||||||
|
index = smallest
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
helpers/queue.go
Normal file
78
helpers/queue.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package aoc
|
||||||
|
|
||||||
|
type Queue[T comparable] struct {
|
||||||
|
items []T
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQueue[T comparable](items []T) *Queue[T] {
|
||||||
|
return &Queue[T]{
|
||||||
|
items: items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue[T]) Len() int {
|
||||||
|
return len(q.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue[T]) Push(item T) {
|
||||||
|
q.items = append(q.items, item)
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) Pop() T {
|
||||||
|
var x T
|
||||||
|
x, q.items = q.items[len(q.items)-1], q.items[:len(q.items)-1]
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) Unshift(item T) {
|
||||||
|
q.items = append([]T{item}, q.items...)
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) Shift() T {
|
||||||
|
var x T
|
||||||
|
x, q.items = q.items[0], q.items[1:]
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue[T]) Append(items []T) {
|
||||||
|
q.items = append(q.items, items...)
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) Copy() *Queue[T] {
|
||||||
|
ret := &Queue[T]{
|
||||||
|
items: make([]T, len(q.items)),
|
||||||
|
}
|
||||||
|
copy(ret.items, q.items)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) Cut(at, count int) []T {
|
||||||
|
ret := q.items[at : at+count]
|
||||||
|
q.items = append(q.items[:at], q.items[at+count:]...)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) Delete(item T) {
|
||||||
|
for i := range q.items {
|
||||||
|
if q.items[i] == item {
|
||||||
|
q.items = append(q.items[:i], q.items[i+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) Expand(at, by int) {
|
||||||
|
q.items = append(q.items[:at], append(make([]T, by), q.items[at:]...)...)
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) Extend(by int) {
|
||||||
|
q.items = append(q.items, make([]T, by)...)
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) FilterInPlace(keep func(item T) bool) {
|
||||||
|
n := 0
|
||||||
|
for _, x := range q.items {
|
||||||
|
if keep(x) {
|
||||||
|
q.items[n] = x
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q.items = q.items[:n]
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) Insert(item T, location int) {
|
||||||
|
q.InsertVector([]T{item}, location)
|
||||||
|
}
|
||||||
|
func (q *Queue[T]) InsertVector(items []T, location int) {
|
||||||
|
q.items = append(q.items[:location], append(items, q.items[location:]...)...)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user