найти кратчайший путь до десяти степеней разделения - PullRequest
9 голосов
/ 17 апреля 2019

У меня есть следующие три таблицы в SQL:

select * from movie limit 2;

  id  |           title            | year | content_rating | duration |    lang    |       country        |  gross   |  budget  | director_id 
------+----------------------------+------+----------------+----------+------------+----------------------+----------+----------+-------------
  407 | 102 Dalmatians             | 2000 | G              |      100 | English    | USA                  | 66941559 | 85000000 |        2174
 3699 | 10 Cloverfield Lane        | 2016 | PG-13          |      104 | English    | USA                  | 71897215 | 15000000 |        1327
(2 rows)

select * from actor limit 3;

  id  |         name         | facebook_likes 
------+----------------------+----------------
  408 | Christian Bale       |          23000
 1430 | Donna Murphy         |            553
   66 | Robert Downey Jr.    |          21000
(3 rows)

select * from acting limit 3;

 movie_id | actor_id 
----------+----------
      407 |     2024
     3699 |     1841
     3016 |       11
(3 rows)

Учитывая двух актеров a1 и a2, я хочу найти кратчайший путь между a1 и a2.

Например, скажем, a1 = 'Tom Cruise' и a2 = 'Robert Downey Jr'.

Вывод должен быть

Tom Cruise was in Days of Thunder with Robert Duvall -> Robert Duvall was in Lucky You with Robert Downey Jr.

В этом случае Tom Cruise был на 2 градуса от Robert Downey Jr, с Robert Durvall, соединяющим их. Самое большее, я хотел бы вывести до 10 градусов , а после этого игнорировать любые соединения.

Я пытался реализовать решение SQL-запрос с 6 степенями разделения для анализа сети с использованием рекурсивного CTE, но я не думаю, что применил его правильно. Помощь приветствуется, спасибо заранее :)

Попытка запроса:

with recursive cte as (
select actor.name, movie.title, 1 as level from movie
left join acting on acting.movie_id = movie.id 
left join actor on actor.id = acting.actor_id
where actor.name = 'Tom Cruise'
union  
select actor.name, movie.title, level+1 from movie
left join acting on acting.movie_id = movie.id 
left join actor on actor.id = acting.actor_id
inner join cte on cte.name = actor.name
where cte.name = actor.name and cte.level < 10
)
select * from cte

Ответы [ 2 ]

5 голосов
/ 20 апреля 2019

Я не уверен, что вернет ваш второй выбор в запросе, но вот способ получить степени разделения между акторами:

Допустим, у нас есть таблица идентификаторов акторов, Origin.Чтобы собрать всех актеров, которые играли в одном фильме с одним из актеров на нашем столе, нам нужно начать с Origin, присоединиться к Acting, а затем Movie, чтобы получить все фильмы, в которых сыграли наши актеры Origin., а затем присоединитесь к Acting снова и таблице Actor, чтобы получить то, что мы хотим.Обратите внимание, что таблица Acting появляется два раза.Если мы применим это к рекурсивному CTE и вашему вопросу, отметив, что для таблицы Origin в вашем примере будет Cte, мы получим следующее:

WITH RECURSIVE cte(id, distance) AS (
    SELECT actor.id, 0 
    FROM actor
    WHERE actor.name = 'Tom Cruise'

    UNION

    SELECT DISTINCT actor.id, cte.distance + 1
    FROM cte
    JOIN acting AS acting1 ON (cte.id = acting1.actor_id)
    JOIN movie ON (acting1.movie_id = movie.id)
    JOIN acting AS acting2 ON (movie.id = acting2.movie_id)
    JOIN actor ON (acting2.actor_id = actor.id)
    WHERE cte.id <> actor.id AND cte.distance + 1 <= 10
)

После этого таблица cte будет содержать кортежи типа(id, dist), что означает, что существует путь от Тома Круза к актеру с этим идентификатором и расстоянием dist.

DISTINCT по соображениям эффективности.В нашей таблице Cte будет много плохих пар (второе значение будет больше, чем истинное расстояние), особенно если граф актера плотный, но правильный кортеж будет в таблице Cte.Под правильным кортежем я подразумеваю кортеж (актер, расстояние), такой, что расстояние - это кратчайший путь между начинающим актером (например, Томом Крузом) и этим актером.

Редактировать: Мой плохой, UNION уже делает это, поэтому DISTINCT не нужен для дубликатов.

Чтобы получить это расстояние, мы добавляем выбор с предложением group by:

WITH RECURSIVE cte(id, distance) AS (
    SELECT actor.id, 0 
    FROM actor
    WHERE actor.name = 'Tom Cruise'

    UNION

    SELECT actor.id, cte.distance + 1
    FROM cte
    JOIN acting AS acting1 ON (cte.id = acting1.actor_id)
    JOIN movie ON (acting1.movie_id = movie.id)
    JOIN acting AS acting2 ON (movie.id = acting2.movie_id)
    JOIN actor ON (acting2.actor_id = actor.id)
    WHERE cte.id <> actor.id AND cte.distance + 1 <= 10
)
SELECT id, MIN(distance) AS distance
FROM cte
GROUP BY id
ORDER BY 2 ASC;

Если вы хотите увидеть результат для данного второго актера, скажем, Роберта Дауни-младшего, то это даст вам ответ относительно степеней разделения:

WITH RECURSIVE cte(id, distance) AS (
    SELECT actor.id, 0 
    FROM actor
    WHERE actor.name = 'Tom Cruise'

    UNION

    SELECT actor.id, cte.distance + 1
    FROM cte
    JOIN acting AS acting1 ON (cte.id = acting1.actor_id)
    JOIN movie ON (acting1.movie_id = movie.id)
    JOIN acting AS acting2 ON (movie.id = acting2.movie_id)
    JOIN actor ON (acting2.actor_id = actor.id)
    WHERE cte.id <> actor.id AND cte.distance + 1 <= 10
), distance_table (id, distance) AS (
    SELECT id, MIN(distance) AS distance
    FROM cte
    GROUP BY id
)
SELECT 'Tom Cruise and ' || actor.name || ' are separated by ' ||
       COALESCE(TO_CHAR(distance_table.distance, '999999'), 'more than 10') || ' degrees of separation'
FROM actor
LEFT JOIN distance_table ON (actor.id = distance_table.id)
WHERE actor.name = 'Robert Downey Jr';

Хотя я неЯ не думаю, что вы, как правило, хотите вычислять такую ​​информацию непосредственно из базы данных, если вы хотите, чтобы сообщение сообщало путь между актерами, подобно тому, который вы предоставили (Том Круз был в «Днях грома» с Робертом Дюваллом ->Роберт Дюваль был в «Удачливом тебе» с Робертом Дауни-младшим), тогда что-то вроде этого могло бы вернуть следующее:

WITH RECURSIVE cte(id, name, distance, message) AS (
    SELECT actor.id, actor.name, 0, ''
    FROM actor
    WHERE actor.name = 'Tom Cruise'

    UNION

    SELECT actor.id, actor.name, cte.distance + 1, 
           cte.message || '> ' || cte.name || ' was in ' ||
           movie.title || ' with ' || actor.name || ' '
    FROM cte
    JOIN acting AS acting1 ON (cte.id = acting1.actor_id)
    JOIN movie ON (acting1.movie_id = movie.id)
    JOIN acting AS acting2 ON (movie.id = acting2.movie_id)
    JOIN actor ON (acting2.actor_id = actor.id)
    WHERE cte.id <> actor.id AND cte.distance + 1 <= 10
), distance_table (id, distance) AS (
    SELECT id, MIN(distance) AS distance
    FROM cte
    GROUP BY id
)
SELECT id, name, message, distance
FROM cte
WHERE (id, distance) IN (SELECT * FROM distance_table)
ORDER BY distance;
4 голосов
/ 21 апреля 2019

Вот удар (без CTE).У меня был список из 4175 пар городов США и США.(Подумайте о состоянии == фильм и город == актер.)

Вот настройки из таблицы us:

SET NAMES utf8 COLLATE utf8_unicode_ci;

DROP TABLE IF EXISTS p_mapping;  -- state-city pairs (movie-actor)
CREATE TABLE p_mapping (
    state char(2) CHARACTER SET ascii NOT NULL,
    city varchar(255)  CHARACTER SET utf8 COLLATE utf8_unicode_ci  NOT NULL,
    PRIMARY KEY(state, city),
    INDEX(city, state)
) ENGINE=InnoDB;
INSERT INTO p_mapping (state, city)
    SELECT state, city  FROM us;

DROP TABLE IF EXISTS p_cities;  -- city ~= actor
CREATE TABLE p_cities (
    depth TINYINT UNSIGNED NOT NULL DEFAULT 0,
    from_state  char(2)  CHARACTER SET ascii  NOT NULL DEFAULT '',
    city  VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
    PRIMARY KEY(city)
) ENGINE=InnoDB COMMENT 'SO 55717636';
INSERT INTO p_cities (city)
    SELECT DISTINCT city  FROM p_mapping;

DROP TABLE IF EXISTS p_states;  -- state ~= movie
CREATE TABLE p_states (
    depth TINYINT UNSIGNED NOT NULL DEFAULT 0,
    from_city  VARCHAR(255)  CHARACTER SET utf8 COLLATE utf8_unicode_ci  NOT NULL  DEFAULT '',
    state char(2)  CHARACTER SET ascii  NOT NULL,
    PRIMARY KEY(state)
) ENGINE=InnoDB COMMENT 'SO 55717636';
INSERT INTO p_states (state)
    SELECT DISTINCT state  FROM p_mapping;

- я выбрал цель связать Омаху (только вНебраска) в Бирмингем (в нескольких штатах.) Сначала начальная инициализация:

SET @city := 'Omaha';   -- starting here

UPDATE p_cities
    SET depth = 1
    WHERE city = @city;
UPDATE p_states  AS s
  JOIN p_mapping AS m  USING(state)
    SET s.from_city = @city,
        s.depth = 1
    WHERE m.city = @city;

SET @depth := 1;

- затем повторять следующие 10 раз или до тех пор, пока row_affered не упадет до 0. Он остановился после 3 итераций.

UPDATE  p_cities AS c
   JOIN p_mapping AS m  USING(city)
   JOIN p_states  AS s  USING(state)
    SET c.from_state = m.state,
        c.depth = s.depth + 1
    WHERE s.depth = @depth
      AND c.depth = 0;

SET @depth := @depth + 1;

UPDATE  p_states AS s
   JOIN p_mapping AS m  USING(state)
   JOIN p_cities AS c   USING(city)
    SET s.from_city = m.city,
        s.depth = c.depth
    WHERE c.depth = @depth
      AND s.depth = 0;

- конец цикла (и конец алгоритма)

- правильный путь: Омаха -> NE -> Колумб -> ОН -> Афины -> AL -> Бирмингем - Обратите внимание, как этот списокответ (но по вертикали):

SELECT * FROM p_cities
    WHERE city in ('Omaha', 'Columbus', 'Athens', 'Birmingham')
    ORDER BY depth;        

    +-------+------------+------------+
    | depth | from_state | city       |
    +-------+------------+------------+
    |     1 |            | Omaha      |
    |     2 | NE         | Columbus   |
    |     3 | OH         | Athens     |
    |     4 | AL         | Birmingham |
    +-------+------------+------------+
    4 rows in set (0.00 sec)

- «Доказательство», что ссылки работают для ответа ниже:

SELECT * FROM p_mapping
    WHERE city IN ('Omaha', 'Columbus', 'Athens', 'Birmingham')
      AND state IN ('NE', 'OH', 'TN', 'AL');

    +-------+------------+
    | state | city       |
    +-------+------------+
    | AL    | Athens     |
    | OH    | Athens     |
    | TN    | Athens     |
    | AL    | Birmingham |
    | NE    | Columbus   |
    | OH    | Columbus   |
    | NE    | Omaha      |
    +-------+------------+
    7 rows in set (0.00 sec)

- (Другая таблица)

SELECT * FROM p_states
    WHERE from_city IN ('Omaha', 'Columbus', 'Athens', 'Birmingham')
       OR state IN ('NE', 'OH', 'TN', 'AL')
    ORDER BY depth;

    +-------+-----------+-------+
    | depth | from_city | state |
    +-------+-----------+-------+
    |     1 | Omaha     | NE    |
    |     2 | Columbus  | GA    |
    |     2 | Columbus  | IN    |
    |     2 | Columbus  | MS    |
    |     2 | Columbus  | OH    |
    |     3 | Athens    | AL    |
    |     3 | Athens    | TN    |
    +-------+-----------+-------+
    7 rows in set (0.00 sec)
...