Buggy python submission (verdict: Time Limit Exceeded):
import sys, itertools
f = sys.stdin
start = [line.split() for line in f]
start = tuple(int(x) for y in start for x in y)
end =(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0)

d = {start:0}

def next_board(b):
    brank = b.index(0)
    if brank % 4:
        yield b[:brank - 1] + (b[brank], b[brank - 1]) + b[brank + 1:]
    if brank // 4:
        yield b[:brank - 4] + (b[brank],)+ b[brank - 3:brank] +  (b[brank - 4],) + b[brank + 1:]
    if brank % 4 != 3:
        yield b[:brank] + (b[brank + 1], b[brank]) + b[brank + 2:]
    if brank // 4 != 3:
        yield b[:brank] + (b[brank + 4],)+ b[brank + 1:brank + 4] +  (b[brank],) + b[brank + 5:]

queue = set([start])
while len(queue):
    u = queue.pop()
    for b in next_board(u):
        if (end not in d or d[end] > d[u]) and (b not in d or d[b] > d[u] + 1):
            d[b] = d[u] + 1
            queue.update([b])
        
print(d[end])

Fixed submission:
from sys import stdin
readline = stdin.readline


GOAL = [i for i in range(1, 16)] + [0]
MAX_DEPTH = 45  # 80
count = 0

# ??£??\?????????
adjacent = (
    (1, 4),          # 0
    (0, 2, 5),       # 1
    (1, 6, 3),       # 2
    (2, 7),          # 3
    (0, 5, 8),       # 4
    (1, 4, 6, 9),    # 5
    (2, 5, 7, 10),   # 6
    (3, 6, 11),      # 7
    (4, 9, 12),      # 8
    (5, 8, 10, 13),  # 9
    (6, 9, 11, 14),  # 10
    (7, 10, 15),     # 11
    (8, 13),         # 12
    (9, 12, 14),     # 13
    (10, 13, 15),    # 14
    (11, 14)         # 15
)

# ???????????????????????¢
distance = (
    (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
    (0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6),
    (1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 4, 5),
    (2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3, 5, 4, 3, 4),
    (3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2, 6, 5, 4, 3),
    (1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5),
    (2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4),
    (3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3),
    (4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2),
    (2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4),
    (3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3),
    (4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2),
    (5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1),
    (3, 4, 5, 6, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3),
    (4, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2),
    (5, 4, 3, 4, 4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1)
)


def manhattan_distance(board):
    return sum(distance[bi][i] for i, bi in enumerate(board))


def answer_is_odd(board):
    return sum(divmod(board.index(0), 4)) % 2


def search(board):
    lower = manhattan_distance(board)
    start_depth = lower
    if (lower % 2) ^ answer_is_odd(board):
        start_depth += 1
    for limit in range(start_depth, MAX_DEPTH + 1, 2):
        id_lower_search(board, limit, 0, board.index(0), None, lower)
        if count > 0:
            return limit


def nxt_board(board, space, prev):
    for nxt in adjacent[space]:
        if nxt == prev:
            continue
        b = board[:]
        b[space], b[nxt] = b[nxt], 0
        yield b, nxt


def id_lower_search(board, limit, move, space, prev, lower):
    if move == limit:
        if board == GOAL:
            global count
            count += 1
    else:
        for b, nxt in nxt_board(board, space, prev):
            p = board[nxt]
            new_lower = lower - distance[p][nxt] + distance[p][space]
            if new_lower + move <= limit:
                id_lower_search(b, limit, move + 1, nxt, space, new_lower)


def main():
    start = (map(int, readline().split()) for _ in range(4))
    start = [y for x in start for y in x]
    print(search(start))
main()

Buggy python submission (verdict: Runtime Error):
import sys, itertools
f = sys.stdin
start = [line.split() for line in f]
start = tuple(int(x) for y in start for x in y)
end =(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0)


def next_board(b):
    brank = b.index(0)
    if brank % 4:
        yield b[:brank - 1] + (b[brank], b[brank - 1]) + b[brank + 1:]
    if brank // 4:
        yield b[:brank - 4] + (b[brank],)+ b[brank - 3:brank] +  (b[brank - 4],) + b[brank + 1:]
    if brank % 4 != 3:
        yield b[:brank] + (b[brank + 1], b[brank]) + b[brank + 2:]
    if brank // 4 != 3:
        yield b[:brank] + (b[brank + 4],)+ b[brank + 1:brank + 4] +  (b[brank],) + b[brank + 5:]

d = {start:1, end:-1}
#queue = set([(start, 1), (end, -1)])
queue = list([(start, 1), (end, -1)])
while len(queue):
    u, way = queue.pop(0)
    for b in next_board(u):
        if b not in d:
            d[b] = d[u] + way
            queue.append((b, way))
        elif d[b] * way < 0:
            print(abs(d[b]) + abs(d[u]) - 1)
            queue.clear()
            break

Fixed submission:
from sys import stdin
readline = stdin.readline


GOAL = [i for i in range(1, 16)] + [0]
MAX_DEPTH = 45  # 80
count = 0

# ??£??\?????????
adjacent = (
    (1, 4),          # 0
    (0, 2, 5),       # 1
    (1, 6, 3),       # 2
    (2, 7),          # 3
    (0, 5, 8),       # 4
    (1, 4, 6, 9),    # 5
    (2, 5, 7, 10),   # 6
    (3, 6, 11),      # 7
    (4, 9, 12),      # 8
    (5, 8, 10, 13),  # 9
    (6, 9, 11, 14),  # 10
    (7, 10, 15),     # 11
    (8, 13),         # 12
    (9, 12, 14),     # 13
    (10, 13, 15),    # 14
    (11, 14)         # 15
)

# ???????????????????????¢
distance = (
    (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
    (0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6),
    (1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 4, 5),
    (2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3, 5, 4, 3, 4),
    (3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2, 6, 5, 4, 3),
    (1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5),
    (2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4),
    (3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3),
    (4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2),
    (2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4),
    (3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3),
    (4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2),
    (5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1),
    (3, 4, 5, 6, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3),
    (4, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2),
    (5, 4, 3, 4, 4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1)
)


def manhattan_distance(board):
    return sum(distance[bi][i] for i, bi in enumerate(board))


def answer_is_odd(board):
    return sum(divmod(board.index(0), 4)) % 2


def search(board):
    lower = manhattan_distance(board)
    start_depth = lower
    if (lower % 2) ^ answer_is_odd(board):
        start_depth += 1
    for limit in range(start_depth, MAX_DEPTH + 1, 2):
        id_lower_search(board, limit, 0, board.index(0), None, lower)
        if count > 0:
            return limit


def nxt_board(board, space, prev):
    for nxt in adjacent[space]:
        if nxt == prev:
            continue
        b = board[:]
        b[space], b[nxt] = b[nxt], 0
        yield b, nxt


def id_lower_search(board, limit, move, space, prev, lower):
    if move == limit:
        if board == GOAL:
            global count
            count += 1
    else:
        for b, nxt in nxt_board(board, space, prev):
            p = board[nxt]
            new_lower = lower - distance[p][nxt] + distance[p][space]
            if new_lower + move <= limit:
                id_lower_search(b, limit, move + 1, nxt, space, new_lower)


def main():
    start = (map(int, readline().split()) for _ in range(4))
    start = [y for x in start for y in x]
    print(search(start))
main()

Buggy python submission (verdict: Runtime Error):
start = [input().split() for _ in range(4)]
start = tuple(int(x) for y in start for x in y)
end =(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0)

def next_board(b):
    brank = b.index(0)
    if brank % 4:
        yield b[:brank - 1] + (b[brank], b[brank - 1]) + b[brank + 1:]
    if brank // 4:
        yield b[:brank - 4] + (b[brank],)+ b[brank - 3:brank] +  (b[brank - 4],) + b[brank + 1:]
    if brank % 4 != 3:
        yield b[:brank] + (b[brank + 1], b[brank]) + b[brank + 2:]
    if brank // 4 != 3:
        yield b[:brank] + (b[brank + 4],)+ b[brank + 1:brank + 4] +  (b[brank],) + b[brank + 5:]

d = {start:1, end:-1}
queue = list([(start, 1), (end, -1)])
while len(queue):
    u, way = queue.pop(0)
    for b in next_board(u):
        if b not in d:
            d[b] = d[u] + way
            queue.append((b, way))
        elif d[b] * way < 0:
            print(abs(d[b]) + abs(d[u]) - 1)
            queue.clear()
            break

Fixed submission:
from sys import stdin
readline = stdin.readline


GOAL = [i for i in range(1, 16)] + [0]
MAX_DEPTH = 45  # 80
count = 0

# ??£??\?????????
adjacent = (
    (1, 4),          # 0
    (0, 2, 5),       # 1
    (1, 6, 3),       # 2
    (2, 7),          # 3
    (0, 5, 8),       # 4
    (1, 4, 6, 9),    # 5
    (2, 5, 7, 10),   # 6
    (3, 6, 11),      # 7
    (4, 9, 12),      # 8
    (5, 8, 10, 13),  # 9
    (6, 9, 11, 14),  # 10
    (7, 10, 15),     # 11
    (8, 13),         # 12
    (9, 12, 14),     # 13
    (10, 13, 15),    # 14
    (11, 14)         # 15
)

# ???????????????????????¢
distance = (
    (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
    (0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6),
    (1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 4, 5),
    (2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3, 5, 4, 3, 4),
    (3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2, 6, 5, 4, 3),
    (1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5),
    (2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4),
    (3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3),
    (4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2),
    (2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4),
    (3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3),
    (4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2),
    (5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1),
    (3, 4, 5, 6, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3),
    (4, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2),
    (5, 4, 3, 4, 4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1)
)


def manhattan_distance(board):
    return sum(distance[bi][i] for i, bi in enumerate(board))


def answer_is_odd(board):
    return sum(divmod(board.index(0), 4)) % 2


def search(board):
    lower = manhattan_distance(board)
    start_depth = lower
    if (lower % 2) ^ answer_is_odd(board):
        start_depth += 1
    for limit in range(start_depth, MAX_DEPTH + 1, 2):
        id_lower_search(board, limit, 0, board.index(0), None, lower)
        if count > 0:
            return limit


def nxt_board(board, space, prev):
    for nxt in adjacent[space]:
        if nxt == prev:
            continue
        b = board[:]
        b[space], b[nxt] = b[nxt], 0
        yield b, nxt


def id_lower_search(board, limit, move, space, prev, lower):
    if move == limit:
        if board == GOAL:
            global count
            count += 1
    else:
        for b, nxt in nxt_board(board, space, prev):
            p = board[nxt]
            new_lower = lower - distance[p][nxt] + distance[p][space]
            if new_lower + move <= limit:
                id_lower_search(b, limit, move + 1, nxt, space, new_lower)


def main():
    start = (map(int, readline().split()) for _ in range(4))
    start = [y for x in start for y in x]
    print(search(start))
main()

Buggy python submission (verdict: Runtime Error):
start = [input().split() for _ in range(4)]
start = [x for y in start for x in y]
start[start.index('10')] = 'a'
start[start.index('11')] = 'b'
start[start.index('12')] = 'c'
start[start.index('13')] = 'd'
start[start.index('14')] = 'e'
start[start.index('15')] = 'f'
start = ''.join(start)

end = '123456789abcdef0'

def next_board(b):
    brank = b.index('0')
    if brank % 4:
        yield b[:brank - 1] + b[brank] + b[brank - 1] + b[brank + 1:]
    if brank // 4:
        yield b[:brank - 4] + b[brank] + b[brank - 3:brank] + b[brank - 4] + b[brank + 1:]
    if brank % 4 != 3:
        yield b[:brank] + b[brank + 1] + b[brank] + b[brank + 2:]
    if brank // 4 != 3:
        yield b[:brank] + b[brank + 4]+ b[brank + 1:brank + 4] + b[brank] + b[brank + 5:]

d = {start:1, end:-1}
queue = collections.deque([(start, 1), (end, -1)])
while len(queue):
    u, way = queue.popleft()
    for b in next_board(u):
        if b not in d:
            d[b] = d[u] + way
            queue.append((b, way))
        elif d[b] * way < 0:
            print(abs(d[b]) + abs(d[u]) - 1)
            queue.clear()
            break

Fixed submission:
from sys import stdin
readline = stdin.readline


GOAL = [i for i in range(1, 16)] + [0]
MAX_DEPTH = 45  # 80
count = 0

# ??£??\?????????
adjacent = (
    (1, 4),          # 0
    (0, 2, 5),       # 1
    (1, 6, 3),       # 2
    (2, 7),          # 3
    (0, 5, 8),       # 4
    (1, 4, 6, 9),    # 5
    (2, 5, 7, 10),   # 6
    (3, 6, 11),      # 7
    (4, 9, 12),      # 8
    (5, 8, 10, 13),  # 9
    (6, 9, 11, 14),  # 10
    (7, 10, 15),     # 11
    (8, 13),         # 12
    (9, 12, 14),     # 13
    (10, 13, 15),    # 14
    (11, 14)         # 15
)

# ???????????????????????¢
distance = (
    (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
    (0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6),
    (1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 4, 5),
    (2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3, 5, 4, 3, 4),
    (3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2, 6, 5, 4, 3),
    (1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5),
    (2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4),
    (3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3),
    (4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2),
    (2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4),
    (3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3),
    (4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2),
    (5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1),
    (3, 4, 5, 6, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3),
    (4, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2),
    (5, 4, 3, 4, 4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1)
)


def manhattan_distance(board):
    return sum(distance[bi][i] for i, bi in enumerate(board))


def answer_is_odd(board):
    return sum(divmod(board.index(0), 4)) % 2


def search(board):
    lower = manhattan_distance(board)
    start_depth = lower
    if (lower % 2) ^ answer_is_odd(board):
        start_depth += 1
    for limit in range(start_depth, MAX_DEPTH + 1, 2):
        id_lower_search(board, limit, 0, board.index(0), None, lower)
        if count > 0:
            return limit


def nxt_board(board, space, prev):
    for nxt in adjacent[space]:
        if nxt == prev:
            continue
        b = board[:]
        b[space], b[nxt] = b[nxt], 0
        yield b, nxt


def id_lower_search(board, limit, move, space, prev, lower):
    if move == limit:
        if board == GOAL:
            global count
            count += 1
    else:
        for b, nxt in nxt_board(board, space, prev):
            p = board[nxt]
            new_lower = lower - distance[p][nxt] + distance[p][space]
            if new_lower + move <= limit:
                id_lower_search(b, limit, move + 1, nxt, space, new_lower)


def main():
    start = (map(int, readline().split()) for _ in range(4))
    start = [y for x in start for y in x]
    print(search(start))
main()

Buggy python submission (verdict: Runtime Error):
import sys
start = [line.split() for line in sys.stdin]
start = [x for y in start for x in y]
start[start.index('10')] = 'a'
start[start.index('11')] = 'b'
start[start.index('12')] = 'c'
start[start.index('13')] = 'd'
start[start.index('14')] = 'e'
start[start.index('15')] = 'f'
start = ''.join(start)

end = '123456789abcdef0'

def next_board(b):
    z = b.index('0')
    if z % 4:
        yield swap(b, z, z - 1)
    if z // 4:
        yield swap(b, z, z - 4)
    if z % 4 != 3:
        yield swap(b, z, z + 1)
    if z // 4 != 3:
        yield swap(b, z, z + 4)

def swap(b, i, j):
    b = list(b)
    b[i], b[j] = b[j], b[i]
    return ''.join(b)
        
d = {start:1, end:-1}
queue = collections.deque([(start, 1), (end, -1)])
while len(queue):
    u, way = queue.popleft()
    for b in next_board(u):
        if b not in d:
            d[b] = d[u] + way
            queue.append((b, way))
        elif d[b] * way < 0:
            print(abs(d[b]) + abs(d[u]) - 1)
            queue.clear()
            break

Fixed submission:
from sys import stdin
readline = stdin.readline


GOAL = [i for i in range(1, 16)] + [0]
MAX_DEPTH = 45  # 80
count = 0

# ??£??\?????????
adjacent = (
    (1, 4),          # 0
    (0, 2, 5),       # 1
    (1, 6, 3),       # 2
    (2, 7),          # 3
    (0, 5, 8),       # 4
    (1, 4, 6, 9),    # 5
    (2, 5, 7, 10),   # 6
    (3, 6, 11),      # 7
    (4, 9, 12),      # 8
    (5, 8, 10, 13),  # 9
    (6, 9, 11, 14),  # 10
    (7, 10, 15),     # 11
    (8, 13),         # 12
    (9, 12, 14),     # 13
    (10, 13, 15),    # 14
    (11, 14)         # 15
)

# ???????????????????????¢
distance = (
    (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
    (0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6),
    (1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 4, 5),
    (2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3, 5, 4, 3, 4),
    (3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2, 6, 5, 4, 3),
    (1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5),
    (2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4),
    (3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3),
    (4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2),
    (2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4),
    (3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3),
    (4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2),
    (5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1),
    (3, 4, 5, 6, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3),
    (4, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2),
    (5, 4, 3, 4, 4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1)
)


def manhattan_distance(board):
    return sum(distance[bi][i] for i, bi in enumerate(board))


def answer_is_odd(board):
    return sum(divmod(board.index(0), 4)) % 2


def search(board):
    lower = manhattan_distance(board)
    start_depth = lower
    if (lower % 2) ^ answer_is_odd(board):
        start_depth += 1
    for limit in range(start_depth, MAX_DEPTH + 1, 2):
        id_lower_search(board, limit, 0, board.index(0), None, lower)
        if count > 0:
            return limit


def nxt_board(board, space, prev):
    for nxt in adjacent[space]:
        if nxt == prev:
            continue
        b = board[:]
        b[space], b[nxt] = b[nxt], 0
        yield b, nxt


def id_lower_search(board, limit, move, space, prev, lower):
    if move == limit:
        if board == GOAL:
            global count
            count += 1
    else:
        for b, nxt in nxt_board(board, space, prev):
            p = board[nxt]
            new_lower = lower - distance[p][nxt] + distance[p][space]
            if new_lower + move <= limit:
                id_lower_search(b, limit, move + 1, nxt, space, new_lower)


def main():
    start = (map(int, readline().split()) for _ in range(4))
    start = [y for x in start for y in x]
    print(search(start))
main()

Buggy python submission (verdict: Runtime Error):
import sys
start = [sys.stdin.readline().split() for _ in range(4)]
start = [x for y in start for x in y]
start[start.index('10')] = 'a'
start[start.index('11')] = 'b'
start[start.index('12')] = 'c'
start[start.index('13')] = 'd'
start[start.index('14')] = 'e'
start[start.index('15')] = 'f'
start = ''.join(start)

end = '123456789abcdef0'

def next_board(b):
    z = b.index('0')
    if z % 4:
        yield swap(b, z, z - 1)
    if z // 4:
        yield swap(b, z, z - 4)
    if z % 4 != 3:
        yield swap(b, z, z + 1)
    if z // 4 != 3:
        yield swap(b, z, z + 4)

def swap(b, i, j):
    b = list(b)
    b[i], b[j] = b[j], b[i]
    return ''.join(b)
        
d = {start:1, end:-1}
queue = collections.deque([(start, 1), (end, -1)])
while len(queue):
    u, way = queue.popleft()
    for b in next_board(u):
        if b not in d:
            d[b] = d[u] + way
            queue.append((b, way))
        elif d[b] * way < 0:
            print(abs(d[b]) + abs(d[u]) - 1)
            queue.clear()
            break

Fixed submission:
from sys import stdin
readline = stdin.readline


GOAL = [i for i in range(1, 16)] + [0]
MAX_DEPTH = 45  # 80
count = 0

# ??£??\?????????
adjacent = (
    (1, 4),          # 0
    (0, 2, 5),       # 1
    (1, 6, 3),       # 2
    (2, 7),          # 3
    (0, 5, 8),       # 4
    (1, 4, 6, 9),    # 5
    (2, 5, 7, 10),   # 6
    (3, 6, 11),      # 7
    (4, 9, 12),      # 8
    (5, 8, 10, 13),  # 9
    (6, 9, 11, 14),  # 10
    (7, 10, 15),     # 11
    (8, 13),         # 12
    (9, 12, 14),     # 13
    (10, 13, 15),    # 14
    (11, 14)         # 15
)

# ???????????????????????¢
distance = (
    (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
    (0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6),
    (1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 4, 5),
    (2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3, 5, 4, 3, 4),
    (3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2, 6, 5, 4, 3),
    (1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5),
    (2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4),
    (3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3),
    (4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2),
    (2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4),
    (3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3),
    (4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2),
    (5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1),
    (3, 4, 5, 6, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3),
    (4, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2),
    (5, 4, 3, 4, 4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1)
)


def manhattan_distance(board):
    return sum(distance[bi][i] for i, bi in enumerate(board))


def answer_is_odd(board):
    return sum(divmod(board.index(0), 4)) % 2


def search(board):
    lower = manhattan_distance(board)
    start_depth = lower
    if (lower % 2) ^ answer_is_odd(board):
        start_depth += 1
    for limit in range(start_depth, MAX_DEPTH + 1, 2):
        id_lower_search(board, limit, 0, board.index(0), None, lower)
        if count > 0:
            return limit


def nxt_board(board, space, prev):
    for nxt in adjacent[space]:
        if nxt == prev:
            continue
        b = board[:]
        b[space], b[nxt] = b[nxt], 0
        yield b, nxt


def id_lower_search(board, limit, move, space, prev, lower):
    if move == limit:
        if board == GOAL:
            global count
            count += 1
    else:
        for b, nxt in nxt_board(board, space, prev):
            p = board[nxt]
            new_lower = lower - distance[p][nxt] + distance[p][space]
            if new_lower + move <= limit:
                id_lower_search(b, limit, move + 1, nxt, space, new_lower)


def main():
    start = (map(int, readline().split()) for _ in range(4))
    start = [y for x in start for y in x]
    print(search(start))
main()

Buggy python submission (verdict: Runtime Error):
start = [input().split() for _ in range(4)]
start = [x for y in start for x in y]
start[start.index('10')] = 'a'
start[start.index('11')] = 'b'
start[start.index('12')] = 'c'
start[start.index('13')] = 'd'
start[start.index('14')] = 'e'
start[start.index('15')] = 'f'
start = ''.join(start)

end = '123456789abcdef0'

def next_board(b):
    z = b.index('0')
    if z % 4:
        yield swap(b, z, z - 1)
    if z // 4:
        yield swap(b, z, z - 4)
    if z % 4 != 3:
        yield swap(b, z, z + 1)
    if z // 4 != 3:
        yield swap(b, z, z + 4)

def swap(b, i, j):
    b = list(b)
    b[i], b[j] = b[j], b[i]
    return ''.join(b)
        
d = {start:1, end:-1}
queue = collections.deque([(start, 1), (end, -1)])
while len(queue):
    u, way = queue.popleft()
    for b in next_board(u):
        if b not in d:
            d[b] = d[u] + way
            queue.append((b, way))
        elif d[b] * way < 0:
            print(abs(d[b]) + abs(d[u]) - 1)
            queue.clear()
            break

Fixed submission:
from sys import stdin
readline = stdin.readline


GOAL = [i for i in range(1, 16)] + [0]
MAX_DEPTH = 45  # 80
count = 0

# ??£??\?????????
adjacent = (
    (1, 4),          # 0
    (0, 2, 5),       # 1
    (1, 6, 3),       # 2
    (2, 7),          # 3
    (0, 5, 8),       # 4
    (1, 4, 6, 9),    # 5
    (2, 5, 7, 10),   # 6
    (3, 6, 11),      # 7
    (4, 9, 12),      # 8
    (5, 8, 10, 13),  # 9
    (6, 9, 11, 14),  # 10
    (7, 10, 15),     # 11
    (8, 13),         # 12
    (9, 12, 14),     # 13
    (10, 13, 15),    # 14
    (11, 14)         # 15
)

# ???????????????????????¢
distance = (
    (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
    (0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6),
    (1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 4, 5),
    (2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3, 5, 4, 3, 4),
    (3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2, 6, 5, 4, 3),
    (1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5),
    (2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4),
    (3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3),
    (4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2),
    (2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4),
    (3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3),
    (4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2),
    (5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1),
    (3, 4, 5, 6, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3),
    (4, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2),
    (5, 4, 3, 4, 4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1)
)


def manhattan_distance(board):
    return sum(distance[bi][i] for i, bi in enumerate(board))


def answer_is_odd(board):
    return sum(divmod(board.index(0), 4)) % 2


def search(board):
    lower = manhattan_distance(board)
    start_depth = lower
    if (lower % 2) ^ answer_is_odd(board):
        start_depth += 1
    for limit in range(start_depth, MAX_DEPTH + 1, 2):
        id_lower_search(board, limit, 0, board.index(0), None, lower)
        if count > 0:
            return limit


def nxt_board(board, space, prev):
    for nxt in adjacent[space]:
        if nxt == prev:
            continue
        b = board[:]
        b[space], b[nxt] = b[nxt], 0
        yield b, nxt


def id_lower_search(board, limit, move, space, prev, lower):
    if move == limit:
        if board == GOAL:
            global count
            count += 1
    else:
        for b, nxt in nxt_board(board, space, prev):
            p = board[nxt]
            new_lower = lower - distance[p][nxt] + distance[p][space]
            if new_lower + move <= limit:
                id_lower_search(b, limit, move + 1, nxt, space, new_lower)


def main():
    start = (map(int, readline().split()) for _ in range(4))
    start = [y for x in start for y in x]
    print(search(start))
main()

Buggy python submission (verdict: Time Limit Exceeded):
import collections
start = [input().split() for _ in range(4)]
start = [x for y in start for x in y]
start[start.index('10')] = 'a'
start[start.index('11')] = 'b'
start[start.index('12')] = 'c'
start[start.index('13')] = 'd'
start[start.index('14')] = 'e'
start[start.index('15')] = 'f'
start = ''.join(start)

end = '123456789abcdef0'

def next_board(b):
    z = b.index('0')
    if z % 4:
        yield swap(b, z, z - 1)
    if z // 4:
        yield swap(b, z, z - 4)
    if z % 4 != 3:
        yield swap(b, z, z + 1)
    if z // 4 != 3:
        yield swap(b, z, z + 4)

def swap(b, i, j):
    b = list(b)
    b[i], b[j] = b[j], b[i]
    return ''.join(b)
        
d = {start:1, end:-1}
queue = collections.deque([(start, 1), (end, -1)])
while len(queue):
    u, way = queue.popleft()
    for b in next_board(u):
        if b not in d:
            d[b] = d[u] + way
            queue.append((b, way))
        elif d[b] * way < 0:
            print(abs(d[b]) + abs(d[u]) - 1)
            queue.clear()
            break

Fixed submission:
from sys import stdin
readline = stdin.readline


GOAL = [i for i in range(1, 16)] + [0]
MAX_DEPTH = 45  # 80
count = 0

# ??£??\?????????
adjacent = (
    (1, 4),          # 0
    (0, 2, 5),       # 1
    (1, 6, 3),       # 2
    (2, 7),          # 3
    (0, 5, 8),       # 4
    (1, 4, 6, 9),    # 5
    (2, 5, 7, 10),   # 6
    (3, 6, 11),      # 7
    (4, 9, 12),      # 8
    (5, 8, 10, 13),  # 9
    (6, 9, 11, 14),  # 10
    (7, 10, 15),     # 11
    (8, 13),         # 12
    (9, 12, 14),     # 13
    (10, 13, 15),    # 14
    (11, 14)         # 15
)

# ???????????????????????¢
distance = (
    (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
    (0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6),
    (1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 4, 5),
    (2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3, 5, 4, 3, 4),
    (3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2, 6, 5, 4, 3),
    (1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5),
    (2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4),
    (3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3),
    (4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2),
    (2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4),
    (3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3),
    (4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2),
    (5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1),
    (3, 4, 5, 6, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3),
    (4, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2),
    (5, 4, 3, 4, 4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1)
)


def manhattan_distance(board):
    return sum(distance[bi][i] for i, bi in enumerate(board))


def answer_is_odd(board):
    return sum(divmod(board.index(0), 4)) % 2


def search(board):
    lower = manhattan_distance(board)
    start_depth = lower
    if (lower % 2) ^ answer_is_odd(board):
        start_depth += 1
    for limit in range(start_depth, MAX_DEPTH + 1, 2):
        id_lower_search(board, limit, 0, board.index(0), None, lower)
        if count > 0:
            return limit


def nxt_board(board, space, prev):
    for nxt in adjacent[space]:
        if nxt == prev:
            continue
        b = board[:]
        b[space], b[nxt] = b[nxt], 0
        yield b, nxt


def id_lower_search(board, limit, move, space, prev, lower):
    if move == limit:
        if board == GOAL:
            global count
            count += 1
    else:
        for b, nxt in nxt_board(board, space, prev):
            p = board[nxt]
            new_lower = lower - distance[p][nxt] + distance[p][space]
            if new_lower + move <= limit:
                id_lower_search(b, limit, move + 1, nxt, space, new_lower)


def main():
    start = (map(int, readline().split()) for _ in range(4))
    start = [y for x in start for y in x]
    print(search(start))
main()