I can think of a few methods, each of which perform differently depending on indexes and your particular database implementation. Some that may look slow can be optimised in ways you may not have imagined and so it's worth trialling them all and comparing execution plans to see what is happening...
Note1: I use GROUP BY rather than DISTINCT, this is because it allows the omptimiser to make use of indexes. I've seen implementations work out that they can turn the DISTINCT in to a GROUP BY, but it's highly worth using GROUP BY in the fist place to be sure. It also gets you thinking about indexes, which is never a bad thing.
Note2: Some queries like this take a while to optimise, as there are many options for the optimiser to evaluate. It is therefore often worth compiling all the different options in to stored procedures and comparing the execution of those stored procedures. This ensures your compare actually Query Time and not different Compile Times.
SELECT
[tree].productID
FROM
products_tree AS [tree]
WHERE
[tree].productID IN (1040,1050,1168)
AND NOT EXISTS (SELECT * FROM products_tree WHERE productID = [tree].productID AND categoryID NOT IN (1040,1050,1168))
GROUP BY
[tree].productID
SELECT
[tree].productID
FROM
products_tree AS [tree]
LEFT OUTER JOIN
(
SELECT
productID
FROM
product_tree
WHERE
productID NOT IN (1040,1050,1168)
GROUP BY
productID
)
AS [ok_products]
ON [ok_products].productID = [tree].productID
WHERE
[tree].productID IN (1040,1050,1168)
AND [ok_products].productID IS NULL
GROUP BY
[tree].productID
SELECT
[tree].productID
FROM
products_tree AS [tree]
GROUP BY
[tree].productID
HAVING
MAX(CASE WHEN [tree].productID IN (1040,1050,1168) THEN 1 ELSE 0 END) = 1
AND MAX(CASE WHEN [tree].productID NOT IN (1040,1050,1168) THEN 1 ELSE 0 END) = 0
There are others, and variations of each, but this should give you a very good start. But I really would stress the use of GROUP BY and the consideration to INDEXES :)