Я запропоную R
рішення, яке кодується дещо не R
так, щоб проілюструвати, як можна підійти до інших платформ.
Занепокоєння R
(як і деяких інших платформ, особливо тих, які надають перевагу функціональному стилю програмування) полягає в тому, що постійне оновлення великого масиву може бути дуже дорогим. Натомість, цей алгоритм підтримує свою власну структуру приватних даних, в якій (a) перераховані всі досі заповнені осередки та (b) всі осередки, доступні для вибору (по всьому периметру заповнених комірок) перераховані. Хоча маніпулювання цією структурою даних є менш ефективною, ніж безпосередньо індексація в масив, зберігаючи змінені дані невеликого розміру, це, швидше за все, займе набагато менше часу на обчислення. (Не було докладено жодних зусиль для його оптимізації R
. Попереднє виділення державних векторів повинно економити деякий час виконання, якщо ви хочете продовжувати працювати всередині R
.)
Код коментується і його слід читати просто. Щоб алгоритм був максимально повним, він не використовує будь-яких доповнень, за винятком кінця для побудови результату. Єдина складна частина полягає в тому, що для ефективності та простоти вона віддає перевагу індексуванню в 2D-сітки за допомогою 1D-індексів. Перетворення відбувається у neighbors
функції, якій потрібна 2D-індексація, щоб зрозуміти, якими можуть бути доступні сусіди комірки, а потім перетворити їх у індекс 1D. Ця конверсія є стандартною, тому я не буду коментувати її далі, крім того, щоб зазначити, що в інших GIS-платформах ви можете змінити ролі індексів стовпців і рядків. (У R
, індекси рядків змінюються до того, як роблять індекси стовпців.)
Для ілюстрації цей код приймає сітку, що x
представляє сушу та річкову особливість важкодоступних точок, починається з певного місця (5, 21) у цій сітці (біля нижнього вигину річки) і розширює її випадковим чином, щоб охопити 250 точок . Загальний час - 0,03 секунди. (Коли розмір масиву збільшується в 10 000 - 3000 рядків на 5000 стовпців, час збільшується лише до 0,09 секунди - коефіцієнт лише 3 або близько того - демонструє масштабованість цього алгоритму.) Замість цього щойно виводить сітку 0, 1 та 2, вона виводить послідовність, з якою були виділені нові комірки. На малюнку найдавніші клітини зелені, що перетворюються через золото в кольори лосося.
Повинно бути очевидним, що використовується восьмиточкове сусідство кожної комірки. Для інших мікрорайонів просто змініть nbrhood
значення на початку expand
: це список зрушень індексу щодо будь-якої комірки. Наприклад, мікрорайон "D4" можна вказати як matrix(c(-1,0, 1,0, 0,-1, 0,1), nrow=2)
.
Очевидно також, що у цього способу розповсюдження є свої проблеми: він залишає отвори позаду. Якщо це не те, що було призначено, існують різні способи виправити цю проблему. Наприклад, збережіть наявні комірки в черзі, щоб найдавніші знайдені комірки також були найдавнішими заповненими. Деяку рандомізацію все ще можна застосувати, але наявні комірки більше не будуть вибиратися з однаковими (рівними) ймовірностями. Іншим, більш складним, способом було б вибір доступних комірок з вірогідністю, які залежать від кількості заповнених сусідів. Як тільки клітина стає оточеною, ви можете зробити її шанс на вибір настільки високим, що мало дірок залишиться незаповненим.
Я закінчу, коментуючи, що це не зовсім стільниковий автомат (CA), який би не переходив по клітинах, а натомість оновлював би цілі ділянки клітин у кожному поколінні. Різниця є тонким: із СА, ймовірність відбору для комірок не була б однорідною.
#
# Expand a patch randomly within indicator array `x` (1=unoccupied) by
# `n.size` cells beginning at index `start`.
#
expand <- function(x, n.size, start) {
if (x[start] != 1) stop("Attempting to begin on an unoccupied cell")
n.rows <- dim(x)[1]
n.cols <- dim(x)[2]
nbrhood <- matrix(c(-1,-1, -1,0, -1,1, 0,-1, 0,1, 1,-1, 1,0, 1,1), nrow=2)
#
# Adjoin one more random cell and update `state`, which records
# (1) the immediately available cells and (2) already occupied cells.
#
grow <- function(state) {
#
# Find all available neighbors that lie within the extent of `x` and
# are unoccupied.
#
neighbors <- function(i) {
n <- c((i-1)%%n.rows+1, floor((i-1)/n.rows+1)) + nbrhood
n <- n[, n[1,] >= 1 & n[2,] >= 1 & n[1,] <= n.rows & n[2,] <= n.cols,
drop=FALSE] # Remain inside the extent of `x`.
n <- n[1,] + (n[2,]-1)*n.rows # Convert to *vector* indexes into `x`.
n <- n[x[n]==1] # Stick to valid cells in `x`.
n <- setdiff(n, state$occupied)# Remove any occupied cells.
return (n)
}
#
# Select one available cell uniformly at random.
# Return an updated state.
#
j <- ceiling(runif(1) * length(state$available))
i <- state$available[j]
return(list(index=i,
available = union(state$available[-j], neighbors(i)),
occupied = c(state$occupied, i)))
}
#
# Initialize the state.
# (If `start` is missing, choose a value at random.)
#
if(missing(start)) {
indexes <- 1:(n.rows * n.cols)
indexes <- indexes[x[indexes]==1]
start <- sample(indexes, 1)
}
if(length(start)==2) start <- start[1] + (start[2]-1)*n.rows
state <- list(available=start, occupied=c())
#
# Grow for as long as possible and as long as needed.
#
i <- 1
indices <- c(NA, n.size)
while(length(state$available) > 0 && i <= n.size) {
state <- grow(state)
indices[i] <- state$index
i <- i+1
}
#
# Return a grid of generation numbers from 1, 2, ... through n.size.
#
indices <- indices[!is.na(indices)]
y <- matrix(NA, n.rows, n.cols)
y[indices] <- 1:length(indices)
return(y)
}
#
# Create an interesting grid `x`.
#
n.rows <- 3000
n.cols <- 5000
x <- matrix(1, n.rows, n.cols)
ij <- sapply(1:n.cols, function(i)
c(ceiling(n.rows * 0.5 * (1 + exp(-0.5*i/n.cols) * sin(8*i/n.cols))), i))
x[t(ij)] <- 0; x[t(ij - c(1,0))] <- 0; x[t(ij + c(1,0))] <- 0
#
# Expand around a specified location in a random but reproducible way.
#
set.seed(17)
system.time(y <- expand(x, 250, matrix(c(5, 21), 1)))
#
# Plot `y` over `x`.
#
library(raster)
plot(raster(x[n.rows:1,], xmx=n.cols, ymx=n.rows), col=c("#2020a0", "#f0f0f0"))
plot(raster(y[n.rows:1,] , xmx=n.cols, ymx=n.rows),
col=terrain.colors(255), alpha=.8, add=TRUE)
З незначними модифікаціями ми можемо перетворити цикл, expand
щоб створити кілька кластерів. Доцільно диференціювати кластери за ідентифікатором, який тут буде працювати 2, 3, ... і т.д.
По-перше, змініть expand
на повернення (a) NA
на першому рядку, якщо є помилка та (b) значення в indices
матриці y
. (Не витрачайте час на створення нової матриці y
з кожним викликом.) З цією зміною циклічне циклічне просте: виберіть випадковий старт, спробуйте розгорнути навколо нього, накопичіть індекси кластерів у indices
разі успіху та повторіть, поки не буде зроблено. Ключовою частиною циклу є обмеження кількості ітерацій, якщо багато сусідніх кластерів не можуть бути знайдені: це робиться за допомогою count.max
.
Ось приклад, коли 60 центрів кластерів вибираються навмання рівномірно.
size.clusters <- 250
n.clusters <- 60
count.max <- 200
set.seed(17)
system.time({
n <- n.rows * n.cols
cells.left <- 1:n
cells.left[x!=1] <- -1 # Indicates occupancy of cells
i <- 0
indices <- c()
ids <- c()
while(i < n.clusters && length(cells.left) >= size.clusters && count.max > 0) {
count.max <- count.max-1
xy <- sample(cells.left[cells.left > 0], 1)
cluster <- expand(x, size.clusters, xy)
if (!is.na(cluster[1]) && length(cluster)==size.clusters) {
i <- i+1
ids <- c(ids, rep(i, size.clusters))
indices <- c(indices, cluster)
cells.left[indices] <- -1
}
}
y <- matrix(NA, n.rows, n.cols)
y[indices] <- ids
})
cat(paste(i, "cluster(s) created.", sep=" "))
Ось результат при застосуванні до сітки розміром 310 на 500 (зроблений достатньо малим і грубим, щоб кластери були видимими). На виконання потрібно дві секунди; на сітці 3100 на 5000 (в 100 разів більше) потрібно більше часу (24 секунди), але терміни масштабування досить добре. (На інших платформах, таких як C ++, терміни навряд чи залежать від розміру сітки.)