Identifying spots of "land" in 2D arrays and identifying where to connect them

Array = {
{0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1},
{0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1},
{0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0},
{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0},
{0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0},
{1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1},
}

I have this 2D array. The 1s represent land and the 0s represent water. I need a script that figures out the best place to put bridges to connect all pieces of land. I don’t need it to create the bridge, I just need it to find the coordinates of the 2 spots at each end of all the bridges. Also, it must have the minimum amount of bridges.

2 Likes

Does this mean that you can determine if the last value in the table before the current index is land, and if the current index is water, it will create the bridge (and vice versa)? If so, this doesn’t seem too hard to do. Just trying to confirm.

Do you mean the X and Y positions in the 2D array?

I mean that in the example above, there are 4 clumps of 1s, meaning that there are 4 pieces of land and there would be only 3 bridges

And yes. it could be either a Vector2 or a table.

You have a graph problem (minimum connectivity). You should first use an algorithm that identifies each separate chunk of land and puts them in a list, such as a list where each entry is a list of the XY coordinates that make up that land. It looks like bridges can only be placed on the borders of each continent, so maybe you only need to save those points.
After that, for each chunk of land take each border piece and “look” in each posible direction to see if you can reach a different chunk. If you can, you need to make sure that chunk is not already reachable by a different combination of bridges. For that you need to keep a graph of which chunks are already connected and use a pathfinding algorithm. If the pathfinding fails, that chunk is not yet connected and you can place the bridge.
This would not guarantee every piece of land gets connected, since you could have something like this:

image

If that’s a problem youll need to make the “look for potential bridge” step more complex, possibly taking into acount some information about the closest other pieces of land.
It is probably easy to find the shortest bridge between two specficic chunks such as 1 and 2 above. If you also want the combination of all bridges to be the “most sensible”, for example by minimizing their lengths total, you’ll also need to look into “minimum spanning tree” algorithms, using the bridge length as the cost input.

1 Like

You laid out the solution perfectly. Thanks for answering.

1 Like

Thought this problem looked like a lot of fun, so I wanted to come up with a solution. Originally I was thinking the same thing as @azqjanna

However, the graph is actually not entirely necessary, it’s more of an optimization to the problem. A simpler brute force method exists where you can simply check the distance from each border cell on an island to all other border cells on all other islands to find the nearest island and the shortest bridge simultaneously.

This method would be slow on a large map, but for a map of the size you’ve provided the problem should be pretty trivial. However, this is where the graph could come in. Say you had 100 islands with 5,000 border tiles, with the brute force method that would be 5,000 ^ 2 (25,000,000) tests to find all of the closest islands and their bridges. That’s likely going to be problematic depending on your use-case.

For a graph solution you could start by defining each island as a vertex on the graph, and for each vertex connect all other vertices to it via edges. Once you’ve done that you could find the minimum spanning tree of the graph via Prim’s, Kruskal’s, or Tarjan’s algorithm. Once you’ve found your minimum spanning tree you can do the same distance test as before on each of the border cells, but this time only to the islands connected to each other. This should vastly simplify the problem.

I went ahead and wrote out the brute force method and created a way to visualize it. On my PC it takes about 0.5ms to find all of the islands, water bodies, and find the nearest connection points between the islands. That said, unless your use-case is significantly more complex I really don’t think this needs further optimization.

local map_array = {
	{0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1},
	{0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1},
	{1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1},
	{0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1},
	{1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1},
	{1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0},
	{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
	{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
	{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
	{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0},
	{0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0},
	{0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0},
	{1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0},
	{1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1},
	{1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1},
	{1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1},
	{1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1},
}

local height = #map_array;
local width = #map_array[1];

local body_types = {
	[0] = 'Water',
	[1] = 'Land',
};

local land_bodies = {};
local water_bodies = {};

local _considered_cells = {};

local function get_cell(x: number, y: number)
	return map_array[y] and map_array[y][x];
end

local function get_body(x: number, y: number, body)	
	local center_cell = get_cell(x, y);
	local body_type = body_types[center_cell];
	
	local body = body or {
		Type = body_type,
		Cells = {},
		BorderCells = {},
		TotalPosition = Vector2.zero,
	};
	
	_considered_cells[x.. ',' ..y] = true;
	
	local value = Vector2.new(x, y);
	
	
	table.insert(body.Cells, value);
	body.TotalPosition += value;
	
	local function check_adjacent(x2, y2)
		if (x2 <= 0 or x2 > width or y2 <= 0 or y2 > height) then
			return; -- Out of map bounds.
		end

		local adjacent_cell = get_cell(x2, y2);
		local adjacent_type = body_types[adjacent_cell];

		if (adjacent_type ~= body_type) then
			body.BorderCells[x.. ',' ..y] = value;
		end

		if (_considered_cells[x2.. ',' ..y2]) then
			return;
		end

		if (adjacent_type == body_type) then
			get_body(x2, y2, body);
		end
	end
	
	check_adjacent(x - 1, y);
	check_adjacent(x, y - 1);
	check_adjacent(x + 1, y);
	check_adjacent(x, y + 1);

	return body;
end

local function get_bodies()
	local bodies = {};
	water_bodies = {};
	land_bodies = {};
	
	for x = 1, width do
		for y = 1, height do
			if (not _considered_cells[x.. ',' ..y]) then
				local body = get_body(x, y);
				body.Position = body.TotalPosition / #body.Cells;
				
				table.insert(bodies, body);
				
				if (body.Type == 'Water') then
					table.insert(water_bodies, body);
				else
					table.insert(land_bodies, body);
				end
			end
		end
	end
	
	return bodies;
end

local start_clock = os.clock();

local bodies = get_bodies();

local island_linkages = {};

local function get_closest_island_cell(island)
	local closest_p1 = nil;
	local closest_p2 = nil;
	local nearest_island = nil;
	local shortest_distance = 0;

	for _, other_island in land_bodies do
		if (other_island == island) then
			continue;
		end

		if (island_linkages[other_island] and island_linkages[other_island].NearestIsland == island) then
			continue;
		end

		for _, p1 in island.BorderCells do
			for _, p2 in other_island.BorderCells do
				local dist = (p1 - p2).Magnitude;
				if (dist < shortest_distance or not closest_p1) then
					closest_p1 = p1;
					closest_p2 = p2;
					shortest_distance = dist;
					nearest_island = other_island;
				end
			end
		end
	end

	island_linkages[island] = { From = closest_p1, To = closest_p2, NearestIsland = nearest_island };

	return closest_p1, closest_p2, nearest_island;
end

for _, island in land_bodies do
	get_closest_island_cell(island);
end

local finish_clock = os.clock() - start_clock;

print('Finished island generation. Took ' ..(finish_clock * 1000).. ' milliseconds. Got ' ..#land_bodies.. ' islands and ' ..#water_bodies.. ' oceans.');

-- Visualization stuff below.

local land_part = workspace.Land;
local water_part = workspace.Water;
local marker_part = workspace.Marker;
local edge_indicator = workspace.EdgeIndicator;

land_part.Parent = nil;
water_part.Parent = nil;
marker_part.Parent = nil;
edge_indicator.Parent = nil;

local visual_cells = {};

for _, body in pairs(bodies) do
	for _, position in body.Cells do
		local p = land_part;

		if (body.Type == 'Water') then
			p = water_part;
		end

		local border_cell = body.BorderCells[position.X.. ',' ..position.Y];

		p = p:Clone();
		p.Position = Vector3.new(position.X * 2, 0, position.Y * 2);

		if (border_cell) then
			print('is border cell')
			p.Color = Color3.new(p.Color.R + 0.1, p.Color.G + 0.1, p.Color.B + 0.1);
		end

		p.Parent = workspace;

		visual_cells[position.X.. ',' ..position.Y] = p;
	end

	local m = marker_part:Clone();
	m.Position = Vector3.new(body.Position.X * 2, 1, body.Position.Y * 2);
	m.Parent = workspace;
end

for island, linkage in island_linkages do
	if (not linkage.NearestIsland) then
		continue;
	end

	local ind = edge_indicator:Clone();

	local a1 = Instance.new('Attachment');
	a1.Parent = workspace.Terrain;
	a1.WorldCFrame = CFrame.new(linkage.From.X * 2, 1, linkage.From.Y * 2);

	local a2 = Instance.new('Attachment');
	a2.Parent = workspace.Terrain;
	a2.WorldCFrame = CFrame.new(linkage.To.X * 2, 1, linkage.To.Y * 2);

	ind.Attachment0 = a1;
	ind.Attachment1 = a2;

	ind.Parent = workspace;

	local c1 = visual_cells[linkage.From.X.. ',' ..linkage.From.Y];
	local c2 = visual_cells[linkage.To.X.. ',' ..linkage.To.Y];

	if (c1 and c2) then
		c1.Color = Color3.new(0, 1, 0);
		c2.Color = Color3.new(1, 0, 0);
	end
end

Should you want only straight bridges you can add a test to get_closest_island_cell to ignore any cells that don’t fit p1.X == p2.X or p1.Y == p2.Y and it should provide only straight line bridges.

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.