Exclude Custom Post Type from shared Custom Taxonomy

You could try using the terms_clauses filter

function wpse156370_terms_clauses( $pieces, $taxonomies, $args ) {
    global $wpdb;
    $pieces['fields'] .= $wpdb->prepare( ', (SELECT COUNT(*) FROM '
        . $wpdb->posts . ' p, ' . $wpdb->term_relationships . ' p_tr'
        . ' WHERE p_tr.object_id = p.ID AND p_tr.term_taxonomy_id = tt.term_taxonomy_id'
        . ' AND p.post_status = %s AND p.post_type = %s) AS post_count', 'publish', $args['type'] );
    $pieces['orderby'] = ' HAVING post_count > 0 ' . $pieces['orderby'];
    return $pieces;
}

and then around the get_categories

    add_filter( 'terms_clauses', 'wpse156370_terms_clauses', 10, 3 );
    $categories = get_categories( $args );
    remove_filter( 'terms_clauses', 'wpse156370_terms_clauses', 10, 3 );

It’s a hack, misusing HAVING (and $pieces['orderby']) as can’t refer to the post_count alias in the WHERE, but … seems to work.

To get the term links to differentiate add a query var to each, eg

echo "<a href="" . add_query_arg( "post_type', $post_type, get_term_link($cat) ) . "'>" , $cat->name , "</a>";

And then check for this in a pre_get_posts action

function wpse156370_pre_get_posts($query) {
    $taxonomy = 'art_categories';
    $post_types = array( 'artists', 'educations' );
    if ($query->is_main_query() && $query->is_tax( $taxonomy ) && ( $post_type = get_query_var( 'post_type' ) ) && in_array( $post_type, $post_types ) ) {
        $query->set( 'post_type', $post_type );
    }
}
add_action( 'pre_get_posts', 'wpse156370_pre_get_posts' );