It's complicated.
Basically, you are doing advanced indexing in case 1 and 2, and basic indexing in case 3. The NumPy docs on advanced indexing state:
Advanced indexing always returns a copy of the data (contrast with basic slicing that returns a view).
So we already know the difference between case 2 and 3. Case 2 is basically equivalent to
b = a[[0, 1]]
b[0, 0] = 98
Since advanced indexing creates a copy, b is not linked to a anymore, and changes are not reflected. In case 3, we have
b = a[0:2]
b[0, 0] = 99
where b is now a view into a (basic indexing), so changes in b are reflected in a.
So what is going on in case 1?
The essential difference is that you cannot split this up into an assignment b = ... and subsequent setitem operation. Instead, you are doing setitem directly on the result on the indexing, which does not create a copy (only getitem operates on copies). So we get the same behavior as in case 2.
The following illustrates this:
Case 1 is equivalent to
setitem(a, [0, 1], 100) # operates directly on a
Case 2 is equivalent to
setitem(
getitem(a, [0, 1]), # this is a copy
[0, 0],
98
)