結果

問題 No.5007 Steiner Space Travel
ユーザー fujikawahiroakifujikawahiroaki
提出日時 2023-04-30 17:38:36
言語 Crystal
(1.11.2)
結果
AC  
実行時間 954 ms / 1,000 ms
コード長 16,940 bytes
コンパイル時間 26,450 ms
コンパイル使用メモリ 266,044 KB
実行使用メモリ 5,408 KB
スコア 8,659,077
最終ジャッジ日時 2023-04-30 17:40:35
合計ジャッジ時間 57,788 ms
ジャッジサーバーID
(参考情報)
judge11 / judge16
純コード判定しない問題か言語
このコードへのチャレンジ
(要ログイン)

テストケース

テストケース表示
入力 結果 実行時間
実行使用メモリ
testcase_00 AC 953 ms
5,176 KB
testcase_01 AC 952 ms
5,328 KB
testcase_02 AC 953 ms
5,152 KB
testcase_03 AC 953 ms
5,256 KB
testcase_04 AC 953 ms
5,136 KB
testcase_05 AC 953 ms
5,076 KB
testcase_06 AC 954 ms
5,136 KB
testcase_07 AC 953 ms
5,272 KB
testcase_08 AC 954 ms
5,136 KB
testcase_09 AC 953 ms
5,092 KB
testcase_10 AC 954 ms
5,172 KB
testcase_11 AC 952 ms
5,144 KB
testcase_12 AC 953 ms
5,324 KB
testcase_13 AC 952 ms
5,332 KB
testcase_14 AC 954 ms
5,140 KB
testcase_15 AC 954 ms
5,144 KB
testcase_16 AC 954 ms
5,064 KB
testcase_17 AC 953 ms
5,140 KB
testcase_18 AC 954 ms
5,312 KB
testcase_19 AC 953 ms
5,176 KB
testcase_20 AC 954 ms
5,208 KB
testcase_21 AC 952 ms
5,232 KB
testcase_22 AC 954 ms
5,284 KB
testcase_23 AC 953 ms
5,236 KB
testcase_24 AC 954 ms
5,408 KB
testcase_25 AC 953 ms
5,196 KB
testcase_26 AC 954 ms
5,284 KB
testcase_27 AC 954 ms
5,140 KB
testcase_28 AC 953 ms
5,064 KB
testcase_29 AC 953 ms
5,140 KB
権限があれば一括ダウンロードができます

ソースコード

diff #

# 定数
NUM_PLANETS      = 100i64
NUM_STATIONS     =   8i64
SUM_POINTS       = NUM_PLANETS + NUM_STATIONS # 総地点数
PLANET_MOVE_COST =    5i64                    # 問題文ではα
COORD_MIN        =    0i64                    # 座標の下限値
COORD_MAX        = 1000i64                    # 座標の上限値
TYPE_PLANET      =    1i64                    # 経由地が惑星
TYPE_STATION     =    2i64                    # 経由地が宇宙ステーション
INF              = 10i64 ** 18                # 距離の初期値など、あり得ない大きな数に使う
TIME_LIMIT       = 0.95                       # 時間制限

# 入力受け取り
# 惑星の数と宇宙ステーションの数、問題文のNとMに対応
start_time = Time.utc
n, m = read_line.split.map(&.to_i64)
# 各惑星の座標
coord_planets = Hash(Int64, Tuple(Int64, Int64)).new
n.times do |i|
  a, b = read_line.split.map(&.to_i64)
  coord_planets[i] = {a, b}
end
cluster_of_planet, centers, clusters = make_cluster(coord_planets)
coord_stations = Hash(Int64, Tuple(Int64, Int64)).new
m.times do |i|
  coord_stations[NUM_PLANETS + i] = centers[i]
end

# 解法を実行し出力
ans_route, coord_stations = solve(coord_planets, coord_stations, cluster_of_planet, clusters, start_time)
coord_stations.each_value do |xy|
  x, y = xy
  puts "#{x} #{y}"
end
puts ans_route.size
ans_route.each do |point|
  type = get_point_type(point)
  point += 1
  point -= NUM_PLANETS if type == TYPE_STATION
  puts "#{type} #{point}"
end

# ソルバ
def solve(coord_planets, coord_stations, cluster_of_planet, clusters, start_time)
  loop_times = 0i64
  change_times = 0i64
  route = cluster_route(coord_planets, coord_stations, cluster_of_planet, clusters)
  cost = calc_route_total_cost(route, coord_planets, coord_stations)
  opt2_route = route
  opt2_route = opt2(coord_planets, coord_stations, route)
  opt2_cost = calc_route_total_cost(opt2_route, coord_planets, coord_stations)
  while (Time.utc - start_time).total_seconds <= 0.3 # TIME_LIMIT
    loop_times += 1
    new_cluster_of_planet, new_centers, new_clusters = make_cluster(coord_planets)
    new_coord_stations = Hash(Int64, Tuple(Int64, Int64)).new
    NUM_STATIONS.times do |station_number|
      new_coord_stations[station_number + NUM_PLANETS] = new_centers[station_number]
    end
    new_route = cluster_route(coord_planets, new_coord_stations, new_cluster_of_planet, new_clusters)
    new_cost = calc_route_total_cost(new_route, coord_planets, coord_stations)
    new_opt2_route = opt2(coord_planets, new_coord_stations, new_route)
    new_opt2_cost = calc_route_total_cost(new_opt2_route, coord_planets, new_coord_stations)
    if new_opt2_cost < opt2_cost
      coord_stations = new_coord_stations
      cluster_of_planet = new_cluster_of_planet
      clusters = new_clusters
      centers = new_centers
      route = new_route
      cost = new_cost
      opt2_cost = new_opt2_cost
      opt2_route = new_opt2_route
      change_times += 1
    end
  end
  while (Time.utc - start_time).total_seconds <= TIME_LIMIT
    loop_times += 1
    target_station = Random.rand(NUM_STATIONS)
    x1, y1 = coord_stations[target_station + NUM_PLANETS]
    amount_of_change_x = Random.rand(-10i64..10i64)
    amount_of_change_y = Random.rand(-10i64..10i64)
    x2 = x1 + amount_of_change_x
    y2 = y1 + amount_of_change_y
    next unless 0i64 <= x2 <= COORD_MAX && 0i64 <= y2 <= COORD_MAX
    new_coord_stations = coord_stations.clone
    new_coord_stations[target_station + NUM_PLANETS] = {x2, y2}
    new_opt2_cost = calc_route_total_cost(opt2_route, coord_planets, new_coord_stations)
    if new_opt2_cost < opt2_cost
      coord_stations = new_coord_stations
      opt2_cost = new_opt2_cost
      change_times += 1
    end
  end
  STDERR.puts "loop_times: #{loop_times}"
  STDERR.puts "change_times: #{change_times}"
  {opt2_route, coord_stations}
end

# 2点間の移動に要するエネルギーを計算
def calc_cost(pos1 : Tuple(Int64, Int64), pos2 : Tuple(Int64, Int64), type1 : Int64, type2 : Int64) : Int64
  x1, y1 = pos1
  x2, y2 = pos2
  euc_dist = ((Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)) ** 2).to_i64
  if type1 == TYPE_PLANET && type2 == TYPE_PLANET
    return (PLANET_MOVE_COST ** 2) * euc_dist
  elsif type1 == TYPE_STATION && type2 == TYPE_STATION
    return euc_dist
  else
    return PLANET_MOVE_COST * euc_dist
  end
end

# 指定した地点が惑星か宇宙ステーションかを判別し、該当する判別番号を返す
# ただし地点番号は問題文と異なり0-indexedであることに注意せよ
def get_point_type(point : Int64)
  point < NUM_PLANETS ? TYPE_PLANET : TYPE_STATION
end

# 各経由地間の距離を計算し、二次元配列にまとめて返す
def make_dist_table(coord_planets : Hash(Int64, Tuple(Int64, Int64)), coord_stations : Hash(Int64, Tuple(Int64, Int64))) : Array(Array(Int64))
  table_size = NUM_PLANETS + NUM_STATIONS
  dist_table = Array.new(table_size) { [0i64] * table_size }
  table_size.times do |i|
    table_size.times do |j|
      pos1 = i < NUM_PLANETS ? coord_planets[i] : coord_stations[i]
      pos2 = j < NUM_PLANETS ? coord_planets[j] : coord_stations[j]
      type1 = get_point_type(i)
      type2 = get_point_type(j)
      dist_table[i][j] = calc_cost(pos1, pos2, type1, type2)
    end
  end
  # ワーシャルフロイド法
  SUM_POINTS.times do |k|
    SUM_POINTS.times do |i|
      SUM_POINTS.times do |j|
        d = dist_table[i][k] + dist_table[k][j]
        dist_table[i][j] = dist_table[i][j] < d ? dist_table[i][j] : d
      end
    end
  end
  dist_table
end

# ダイクストラ法によって経由地iから経由地jへの最短経路を復元
def dijkstra(i : Int64, j : Int64, coord_planets : Hash(Int64, Tuple(Int64, Int64)), coord_stations : Hash(Int64, Tuple(Int64, Int64)))
  dists = [INF] * SUM_POINTS
  prev_points = [-1i64] * SUM_POINTS
  q = AtCoder::PriorityQueue(Tuple(Int64, Int64)).new { |a, b| a >= b }
  dists[i] = 0i64
  q << {0i64, i}
  until q.empty?
    dist, point = q.pop.not_nil!
    next if dist > dists[point]
    SUM_POINTS.times do |next_point|
      type1 = get_point_type(point)
      type2 = get_point_type(next_point)
      pos1 = type1 == TYPE_PLANET ? coord_planets[point] : coord_stations[point]
      pos2 = type2 == TYPE_PLANET ? coord_planets[next_point] : coord_stations[next_point]
      next_dist = dist + calc_cost(pos1, pos2, type1, type2)
      if next_dist < dists[next_point]
        prev_points[next_point] = point
        dists[next_point] = next_dist
        q << {next_dist, next_point}
      end
    end
  end
  point = j
  path = [] of Int64
  while point != i
    path << point
    point = prev_points[point]
  end
  path.reverse!
end

# 経路の総コストを算出する
def calc_route_total_cost(route, coord_planets, coord_stations)
  cost = 0i64
  route.each_cons_pair do |a, b|
    type1 = get_point_type(a)
    type2 = get_point_type(b)
    pos1 = type1 == TYPE_PLANET ? coord_planets[a] : coord_stations[a]
    pos2 = type2 == TYPE_PLANET ? coord_planets[b] : coord_stations[b]
    cost += calc_cost(pos1, pos2, type1, type2)
  end
  cost
end

# 距離の近い惑星同士のクラスタを生成
def make_cluster(coord_planets)
  # k-means法もどき
  cluster_of_planet = Hash(Int64, Int64).new
  NUM_PLANETS.times do |planet|
    cluster_of_planet[planet] = Random.rand(NUM_STATIONS)
  end
  clusters = Array.new(NUM_STATIONS) { [] of Int64 }
  cluster_of_planet.each do |k, v|
    clusters[v] << k
  end
  centers = [] of Tuple(Int64, Int64)
  range = 30
  NUM_STATIONS.times do
    x1, y1 = coord_planets.sample[1]
    x2 = Random.rand((x1 - range)..(x1 + range))
    y2 = Random.rand((y1 - range)..(y1 + range))
    x2 = x1 unless 0i64 <= x2 <= COORD_MAX
    y2 = y1 unless 0i64 <= y2 <= COORD_MAX
    centers << {x2, y2}
  end
  finish = false
  until finish
    prev_centers = centers
    new_clusters = Array.new(NUM_STATIONS) { [] of Int64 }
    old_cluster_of_planet = cluster_of_planet.clone
    NUM_PLANETS.times do |planet|
      planet_pos = coord_planets[planet]
      dists = centers.map_with_index { |center, cluster_number|
        x2, y2 = center
        {calc_cost(planet_pos, center, TYPE_PLANET, TYPE_STATION), cluster_number}
      }.sort!
      dists.each do |dist_and_cluster_number|
        dist, cluster_number = dist_and_cluster_number
        new_clusters[cluster_number] << planet
        cluster_of_planet[planet] = cluster_number
        break
      end
    end
    if new_clusters.includes?([] of Int64)
      return {old_cluster_of_planet, centers, clusters}
    end
    clusters = new_clusters
    centers = [] of Tuple(Int64, Int64)
    clusters.each do |cluster|
      centers << calc_center_of_cluster(cluster, coord_planets)
    end
    finish = true if centers == prev_centers
  end
  {cluster_of_planet, centers, clusters}
end

# クラスタの中心座標を求める
def calc_center_of_cluster(cluster, coord_planets)
  center = [0.0, 0.0]
  cluster_size = cluster.size
  cluster.each do |point|
    center[0] += coord_planets[point][0]
    center[1] += coord_planets[point][1]
  end
  center[0] /= cluster_size unless center[0] == 0
  center[1] /= cluster_size unless center[0] == 0
  {center[0].to_i64, center[1].to_i64}
end

# クラスタのコストの合計値を求める
def calc_cluster_total_cost(clusters, coord_planets, coord_stations)
  total_cost = 0i64
  NUM_STATIONS.times do |cluster_number|
    pos1 = coord_stations[cluster_number + NUM_PLANETS]
    total_cost += clusters[cluster_number].sum { |planet|
      calc_cost(pos1, coord_planets[planet], TYPE_STATION, TYPE_PLANET)
    }
  end
  total_cost
end

# 各宇宙ステーションを一周しながら周辺の惑星を訪問し初期解を作る
def cluster_route(coord_planets, coord_stations, cluster_of_planet, clusters)
  current_station = cluster_of_planet[0]
  visited = [false] * NUM_STATIONS
  visited[current_station] = true
  route = [0i64]
  (NUM_STATIONS).times do
    nearest_dist = INF
    nearest_station = -1i64
    NUM_STATIONS.times do |next_station|
      next if visited[next_station]
      dist = calc_cost(coord_stations[current_station + NUM_PLANETS], coord_stations[next_station + NUM_PLANETS], TYPE_STATION, TYPE_STATION)
      if dist < nearest_dist
        nearest_dist = dist
        nearest_station = next_station
      end
    end
    route << current_station + NUM_PLANETS
    clusters[current_station].each do |planet|
      route << planet if planet != 0
      route << current_station + NUM_PLANETS
    end
    route << current_station + NUM_PLANETS
    current_station = nearest_station
    visited[current_station] = true
  end
  route << (cluster_of_planet[0] + NUM_PLANETS)
  route << 0i64
  route
end

# 単純な貪欲法で初期解を作る
def greedy_route(coord_planets : Hash(Int64, Tuple(Int64, Int64)), coord_stations : Hash(Int64, Tuple(Int64, Int64)), dist_table)
  point = 0i64
  visited = [false] * SUM_POINTS
  visited[0] = true
  route = [0i64]
  (SUM_POINTS - 1).times do |i|
    nearest_dist = INF
    nearest_point = -1i64
    SUM_POINTS.times do |next_point|
      next if visited[next_point]
      if dist_table[point][next_point] < nearest_dist
        nearest_dist = dist_table[point][next_point]
        nearest_point = next_point
      end
    end
    path = dijkstra(point, nearest_point, coord_planets, coord_stations)
    route += path
    point = nearest_point
    visited[point] = true
  end
  route += dijkstra(point, 0i64, coord_planets, coord_stations)
  route
end

# 2-opt法で経路を改善
def opt2(coord_planets : Hash(Int64, Tuple(Int64, Int64)), coord_stations : Hash(Int64, Tuple(Int64, Int64)), route_arg)
  total_dist = 0i64
  route = route_arg.clone
  route_size = route.size
  loop do
    cnt = 0i64
    (route_size - 2).times do |i|
      i1 = i + 1
      (i + 2).upto(route_size - 1) do |j|
        j1 = j == route_size - 1 ? 0i64 : j + 1
        if i != 0 && j1 != 0
          point1 = route[i]
          point2 = route[j]
          point3 = route[i1]
          point4 = route[j1]
          type1 = get_point_type(point1)
          type2 = get_point_type(point2)
          type3 = get_point_type(point3)
          type4 = get_point_type(point4)
          pos1 = type1 == TYPE_PLANET ? coord_planets[point1] : coord_stations[point1]
          pos2 = type2 == TYPE_PLANET ? coord_planets[point2] : coord_stations[point2]
          pos3 = type3 == TYPE_PLANET ? coord_planets[point3] : coord_stations[point3]
          pos4 = type4 == TYPE_PLANET ? coord_planets[point4] : coord_stations[point4]
          l1 = calc_cost(pos1, pos3, type1, type3) # dist_table[route[i]][route[i1]]
          l2 = calc_cost(pos2, pos4, type2, type4) # dist_table[route[j]][route[j1]]
          l3 = calc_cost(pos1, pos2, type1, type2) # dist_table[route[i]][route[j]]
          l4 = calc_cost(pos3, pos4, type3, type4) # dist_table[route[i1]][route[j1]]
          if l1 + l2 > l3 + l4
            section_size = j - i1 + 1
            reverse_times = section_size // 2
            route[i1..j] = route[i1..j].reverse!
            cnt += 1
          end
        end
      end
    end
    total_dist += cnt
    break if cnt == 0
  end
  route
end

# 経路の部分破壊と再構築
def change_route(route, coord_planets, coord_stations)
  route_size = route.size
  section_size = Random.rand(2i64..4i64)
  change_start_i = Random.rand(route_size)
  section_limit = change_start_i + section_size < route_size - 1 ? change_start_i + section_size : route_size - 1
  change_end_i = Random.rand(change_start_i..section_limit)
  section = route[change_start_i..change_end_i]
  cost = calc_route_total_cost(section, coord_planets, coord_stations)
  ans_section = section
  changed_route = section
  section.permutations(section.size).to_a.each do |new_seciton|
    new_cost = calc_route_total_cost(new_seciton, coord_planets, coord_stations)
    if new_cost < cost
      ans_section = new_seciton
      cost = new_cost
    end
  end
  new_route = route.clone
  new_route[change_start_i..change_end_i] = ans_section
  new_route
end

# ac-library.cr by hakatashi https://github.com/google/ac-library.cr
#
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module AtCoder
  # Implements standard priority queue like [std::priority_queue](https://en.cppreference.com/w/cpp/container/priority_queue).
  #
  # ```
  # q = AtCoder::PriorityQueue(Int64).new
  # q << 1_i64
  # q << 3_i64
  # q << 2_i64
  # q.pop # => 3
  # q.pop # => 2
  # q.pop # => 1
  # ```
  class PriorityQueue(T)
    getter heap : Array(T)

    def initialize
      initialize { |a, b| a <= b }
    end

    # Initializes queue with the custom comperator.
    #
    # If the second argument `b` should be popped earlier than
    # the first argument `a`, return `true`. Else, return `false`.
    #
    # ```
    # q = AtCoder::PriorityQueue(Int64).new { |a, b| a >= b }
    # q << 1_i64
    # q << 3_i64
    # q << 2_i64
    # q.pop # => 1
    # q.pop # => 2
    # q.pop # => 3
    # ```
    def initialize(&block : T, T -> Bool)
      @heap = Array(T).new
      @compare_proc = block
    end

    # Pushes value into the queue.
    def push(v : T)
      @heap << v
      index = @heap.size - 1
      while index != 0
        parent = (index - 1) // 2
        if @compare_proc.call(@heap[index], @heap[parent])
          break
        end
        @heap[parent], @heap[index] = @heap[index], @heap[parent]
        index = parent
      end
    end

    # Alias of `push`
    def <<(v : T)
      push(v)
    end

    # Pops value from the queue.
    def pop
      if @heap.size == 0
        return nil
      end
      if @heap.size == 1
        return @heap.pop
      end
      ret = @heap.first
      @heap[0] = @heap.pop
      index = 0
      while index * 2 + 1 < @heap.size
        child = if index * 2 + 2 < @heap.size && !@compare_proc.call(@heap[index * 2 + 2], @heap[index * 2 + 1])
                  index * 2 + 2
                else
                  index * 2 + 1
                end
        if @compare_proc.call(@heap[child], @heap[index])
          break
        end
        @heap[child], @heap[index] = @heap[index], @heap[child]
        index = child
      end
      ret
    end

    # Returns `true` if the queue is empty.
    delegate :empty?, to: @heap

    # Returns size of the queue.
    delegate :size, to: @heap
  end
end
0