You can rewrite your query to only access the foo table once instead of twice, by using the MAX-KEEP aggregate function.
An example:
SQL> var N number
SQL> exec :N := 19
PL/SQL-procedure is geslaagd.
SQL> select max(y) keep (dense_rank last order by x) y
  2    from foo
  3   where x <= :N
  4  /
Y
-
a
1 rij is geselecteerd.
SQL> exec :N := 20
PL/SQL-procedure is geslaagd.
SQL> select max(y) keep (dense_rank last order by x) y
  2    from foo
  3   where x <= :N
  4  /
Y
-
b
1 rij is geselecteerd.
SQL> exec :N := 21
PL/SQL-procedure is geslaagd.
SQL> select max(y) keep (dense_rank last order by x) y
  2    from foo
  3   where x <= :N
  4  /
Y
-
b
1 rij is geselecteerd.
Also a,b,b as a result. The query plans:
SQL> set serveroutput off
SQL> select /*+ gather_plan_statistics */
  2         y
  3    from foo
  4   where x = (select max(x) from foo where x<=:N)
  5  /
Y
-
b
1 rij is geselecteerd.
SQL> select * from table(dbms_xplan.display_cursor(null,null,'predicate -note last'))
  2  /
PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------------
SQL_ID  3kh85qqnb2phy, child number 0
-------------------------------------
select /*+ gather_plan_statistics */        y   from foo  where x =
(select max(x) from foo where x<=:N)
Plan hash value: 763646971
----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |       |       |     8 (100)|          |
|*  1 |  TABLE ACCESS FULL  | FOO  |     1 |    16 |     4   (0)| 00:00:01 |
|   2 |   SORT AGGREGATE    |      |     1 |    13 |            |          |
|*  3 |    TABLE ACCESS FULL| FOO  |     2 |    26 |     4   (0)| 00:00:01 |
----------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("X"=)
   3 - filter("X"<=:N)
22 rijen zijn geselecteerd.
SQL> select max(y) keep (dense_rank last order by x) y
  2    from foo
  3   where x <= :N
  4  /
Y
-
b
1 rij is geselecteerd.
SQL> select * from table(dbms_xplan.display_cursor(null,null,'predicate -note last'))
  2  /
PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------------
SQL_ID  avm2zh62c8cwd, child number 0
-------------------------------------
select max(y) keep (dense_rank last order by x) y   from foo  where x
<= :N
Plan hash value: 3274996510
---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |       |       |     4 (100)|          |
|   1 |  SORT AGGREGATE    |      |     1 |    16 |            |          |
|*  2 |   TABLE ACCESS FULL| FOO  |     1 |    16 |     4   (0)| 00:00:01 |
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter("X"<=:N)
20 rijen zijn geselecteerd.
Two full table scans on foo, against one for the new query.
Regards,
Rob.