Código:
 int a[NUM_FIL][NUM_COL], (*p)[NUM_COL], i;
for(p=&a[0]; p<&a[NUM_FIL]; p++)
        (*p)[i]=0;
  int a[NUM_FIL][NUM_COL]; es un array bidimensional que puede interpretarse como de NUM_FIL filas de NUM_COL columnas cada una, aunque sólo como una analogía; en memoria se va a tener una sucesión densa de NUM_FIL arrays de NUM_COL elementos cada uno.  
(*p)[NUM_COL]; es un puntero a un array de NUM_COL elementos.  
Entonces, 
p puede apuntar en forma segura a cada uno de los NUM_FIL arrays de 
a, pero no más. En el ejemplo: 
p = &a[0] asigna a p la dirección de memoria del primer array de NUM_COL en a.  
A la pregunta de si es correcta la asignación p = &a[0], mi respuesta es sí, es correcta, porque se está asignando la dirección del primer array de NUM_COL elementos de a al puntero a array de NUM_COL elementos p. 
Y es incorrecta la asignación p = a[0], porque p y a[0] son de tipos diferentes: 
p es un puntero a un array de NUM_COL elementos, mientras que a[0] es un un array de NUM_COL elementos.