In order to treat a search expression passed to the regex-based -replace operator as a literal string, you must escape regex metacharacters such as ?, which you can do programmatically by calling [regex]::Escape() (in a literal regex string, you could manually escape indiv. chars. with \):
(Get-Content exp.txt) -replace [regex]::Escape($ar[$i]), '$&~'
Note how I've used '...' (single-quoting) for the replacement operand, which is preferable so as to avoid confusion with the string expansion that PowerShell itself performs up front in "..." (double-quoted) strings. Similarly, '...' is preferable for a literal search regex.
Sticking with the regex-based -replace operator in your case makes sense for two reasons:
You want to (directly and succinctly) refer to whatever was matched by the regex in the replacement expression, via placeholder $&.
You want to perform replacements on an array of input strings in a single operation (like many PowerShell operators, -replace supports an array of strings as its LHS, and then operates on each element).
By contrast, in cases where literal string substitutions with a single input string is needed, use of the [string] type's .Replace() method is the simpler and faster option, but note that it is case-sensitive by default (unlike -replace).
See this answer for details.