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.