How can I decrease the time complexity and increase efficiency, without writing a new algorithm. My solution solves the majority of puzzles in a fast time, but for some difficult ones it can take over a minute! I have added some of the difficult puzzles below the code.  

```
import numpy as np
from skimage.util import view_as_blocks  # pip install scikit-image

#input: 9x9 numpy array, empty cell = 0 
#output: 9x9 numpy array: if not solution all array entries should be -1
#backtracking depth-first search with constraint propagation




#searches for 0 value in board
def zero_search(sudoku):
    for i in range(len(sudoku)):
        for j in range(len(sudoku[0])):
            if sudoku[i][j] == 0:
                return (i, j)  # row, col

    return False



#parameters: board, num = inserted value, pos = board position(vector/tuple)
def valid(sudoku, num, pos):
    # Check row
    for i in range(len(sudoku[0])):
        if sudoku[pos[0]][i] == num and pos[1] != i:
            return False

    # Check column
    for i in range(len(sudoku)):
        if sudoku[i][pos[1]] == num and pos[0] != i:
            return False

    # Check box
    box_x = pos[1] // 3
    box_y = pos[0] // 3

    #y-axis/columns 
    for i in range(box_y*3, box_y*3 + 3):
        #x-axis/rows
        for j in range(box_x * 3, box_x*3 + 3):
            if sudoku[i][j] == num and (i,j) != pos:
                return False

    return True


def solver(sudoku):
    
    find = zero_search(sudoku)
    if not find:
        return True
    
    else:
        row, col = find

    #check insertion values 1-9
    for i in range(1,10):
        if valid(sudoku, i, (row, col)):
            sudoku[row][col] = i
            
            if solver(sudoku):
                return True

            sudoku[row][col] = 0
            
    return False 
    
    
    
def initial_invalid(sudoku):

    # Check row
    for i in range(9):
        dup_lst=[]
        for j in range(9):
            if sudoku[j][i]!=0:
                if sudoku[j][i] in dup_lst:
                    return True

                else:
                    dup_lst.append(sudoku[j][i])
     
    #check column
    for i in range(9):
        dup_lst=[]
        for j in range(9):
            if sudoku[i][j]!=0:
                if sudoku[i][j] in dup_lst:
                    return True

                else:
                    dup_lst.append(sudoku[i][j])
                    
                    
    

#not needed
def final_valid (sudoku):

    subgrids = view_as_blocks(sudoku, (3, 3))

    sums = [np.sum(subgrids[i][j]) for j in range(3) for i in range(3)]
    if sum(sums) !=405:
        return False
    
    else:
        return True
    
    #row_sum = sudoku.sum(axis=1)
    #if sum(row_sum) != 405:
    #   return False

    #col_sum = sudoku.sum(axis=0)
    #if sum(col_sum) != 405:
    #    return False
        

    
def sudoku_solver(sudoku):

    if initial_invalid(sudoku):
        return np.full((9,9),-1)
        
    if solver(sudoku):
            return sudoku
    else:
        return np.full((9,9),-1)
```

```python
[[0 8 0 4 3 0 0 0 0]
 [0 0 5 0 0 9 0 0 0]
 [6 0 0 0 8 0 0 7 0]
 [0 0 0 0 9 0 0 0 3]
 [0 0 0 8 0 7 0 0 0]
 [9 0 0 0 0 0 0 5 4]
 [0 6 0 0 0 0 0 0 5]
 [0 0 8 0 0 0 4 0 0]
 [0 4 0 0 0 6 0 1 0]]


[[0 0 2 0 0 0 0 0 4]
 [0 5 0 0 1 3 7 0 0]
 [7 9 0 0 0 0 0 5 0]
 [0 0 9 0 0 0 0 6 0]
 [0 0 0 0 3 0 5 0 8]
 [5 0 7 0 0 0 4 0 0]
 [0 0 0 0 6 0 8 0 0]
 [0 6 0 0 2 7 0 4 0]
 [8 0 0 0 0 0 0 2 0]]


[[0 0 0 6 0 0 2 0 0]
 [0 0 0 0 0 9 0 6 0]
 [0 8 0 0 0 5 0 0 3]
 [1 0 0 4 0 0 9 0 0]
 [8 3 0 0 0 0 0 0 0]
 [0 2 0 0 0 6 0 0 0]
 [0 0 0 0 6 0 0 0 0]
 [2 5 0 3 0 7 0 9 0]
 [0 0 1 0 0 0 0 8 4]]