我想给网格做个格言。这意味着在给定一个n x m的网格时,该网格具有黄色和红色的正方形,我希望以这种方式对网格进行划分,使得黄色将成为尽可能多的部分中的多数颜色,如下图所示:
所有分区都必须是连续的,并且正方形的数目相同,并且所有正方形都必须是彩色的(尽管如果算法可以推广到一些未着色的正方形的网格,那将是很棒的。
我不确定如何对这个问题进行“算术化”,过去蛮力强加了每个可能的分区,而分区本身本身就足够困难,而且效率极低。
完成此任务的最佳方法是什么?
答案 0 :(得分:2)
tl; dr:使用模拟退火,在区域之间交换选民。 底部的演示使您可以执行交换步骤(随机演进)并针对满足需求的区域进行优化(退火)
我们可以将其视为优化问题,我们试图在其中最大化区域数 红色赢得胜利,并最小化蓝色赢得胜利的地区数。
我们将其正式化:
function heuristic(state) {
return state.districts_that_red_wins - state.districts_that_blue_wins;
}
其中state
是各地区选民的分配。
这将起作用,但可以进行一些改进。 让我们介绍wasted votes的概念,以朝着正确的方向轻推我们的优化。 我们想要最大化浪费的蓝票,最小化浪费的红票。 我随心所欲地将它们加权为一个区的1/10,因为每个区有10个选民。 这给了我们最大化的功能:
function heuristic(state) {
let {red_wins, blue_wins, tied, wasted_red_votes, wasted_blue_votes} = get_winners(state, voters);
return red_wins - blue_wins - 0.1 * wasted_red_votes + 0.1 * wasted_blue_votes;
}
您可能想优化其他方面,例如地区的紧凑性。您可以将它们添加到试探函数中。
让我们选择一种优化算法来优化state
。
我们受到一些约束,这使得很难生成适合这些条件的随机地图。
而且我怀疑没有蛮力就不可能找到最佳的区划,而这是不可能的。
因此,让我们使用一种算法来迭代改进我们的解决方案。
我喜欢simulated annealing,因为它易于实现和理解,并且通过防止我们陷入早期的局部最优中而比爬山更好。
我的温度函数就是max(0.8 - iterations/total_iterations, 0)
。刚开始时,只有在情况变得更好时,我们才会有20%的时间采用新的状态;其他80%的人都会采用新状态。
直到我们完成计算预算的80%为止,这逐渐变得像爬山,然后我们才改变状态,前提是它提高了启发式得分。 80%的选择完全是任意的。
要实现SA,我们需要一个初始状态(或生成它的一种方式)。为了简单起见,我将使用“ Perfect Representation”作为初始状态,主要是因为我不知道如何生成随机连接的,大小相等的区域。 我们还需要一种对状态进行小的更改的方法。我将在下一部分中讨论。 最后,我们需要一种对州进行评分的方法。让我们使用上一部分中的函数,因为它的计算非常便宜。
如果您有兴趣,请查看anneal
函数,或者只是阅读Wikipedia文章。
对于这个问题,给定一个状态,我们需要找到另一个不会改变启发式得分的相似状态,以查看我们是否朝着正确的方向前进。 我选择从两个不同的区域中找到一对点,并交换它们。
我们需要保持一些不变性:
第二个很简单:总是交换点,从不(永久)从一个区分配给另一个区。 首先是棘手的,我们需要简要介绍图论。 铰接点(参见图片)是无法将图形两等分而无法删除的点。 对我们来说,这意味着我们不能在不使区域不连续的情况下移除连接点。 一旦有了可以删除的点,就需要确保将其添加到与其相邻的区域中。这很简单。
由于我们在网格上,并且所有区域都必须是连续的,所以我们可以考虑某个点的直接邻居来确定它是否是一个铰接点。 如果看不到它,那不是很重要,可以使用通常在图形上起作用的algorithm。 我发现网格版本更容易,因为它不涉及递归。
如果有兴趣,请参见is_swappable
函数。这就是演示中的“随机演化”按钮。
高层,用于演化状态的代码应如下所示:
function evolve_state() {
randomly pick a source district
randomly pick a non-articulation point, source_point, from source_district
for each neighbour of the articulation point
if the neighbour is in a different district target_district
temporarily remove source_point from source_district and add it to target_district
if any articulation point (other than source point), target_point, in target_district is adjacent to source_district
swap target_point and source_point
return;
restore source_point
}
注意:我以一种随机迭代方式遍历所有source_district
,source_point
,neighbour
,target_district
和target_point
的方式来实现此目的,因为我不是确定这将是多么稀疏。
如果您确切地实现此伪代码,则可能需要比我收敛到解决方案更多的迭代。
如果有兴趣,请参见evolve_state
。
我没有提到的每个函数都是实用程序函数或用于绘图的函数。
现在进行演示。 :) (将Lodash用于实用程序功能,将Mithril用于DOM操作)
如果您想使用此工具,可以更方便地使用我的Plunker:http://plnkr.co/edit/Bho4qhQBKRShXWX8fHmt。
const RED = 'R';
const BLUE = 'B';
const VOTERS_PER_DISTRICT = 10;
const neighbours = [{x: 1, y: 0}, {x: 0, y: 1}, {x: -1, y: 0}, {x: 0, y: -1}];
/* UTILITY FUNCTIONS */
/**
Create a generator that starts at a random point p, 0 <= p < max
The generator will iterate over values p, p+1, ... max, 0, ... p-1
*/
function* cyclic_generator(max) {
let start = _.random(max);
for (let i=0; i<max; i++) {
yield (i + start) % max;
}
}
/**
Return grid[x][y] if x and y are within grid. Otherwise return undefined
*/
function grid_get(grid, x, y) {
if(_.isUndefined(grid[x])) {
return undefined;
}
else {
return grid[x][y];
}
}
/** Generates a 2d array red and blue voters */
function generate_voters() {
return _.times(5, x => _.times(10, () => {return {vote: x > 2 ? RED : BLUE, district_vote: 0xffffff}}))
}
/** Generate an initial state */
function generate_initial_state() {
return _.range(5).map(x => _.range(10).map(y => {return {x, y}}));
}
/**
Randomly swap two squares in the grid between two districts.
The new square to be added must be connected to the district, and the
old square must not break another district in two
*/
function evolve_state(state) {
state = _.cloneDeep(state);
// Create a grid with the district number
let point_to_district = _.range(5).map(x => _.range(10).map(y => -1));
state.forEach((district, i) => district.forEach(({x, y}) => point_to_district[x][y] = i));
// swap a point from source_district to target_district.
// then swap a point from target_district to source_district.
for(let source_district_idx of cyclic_generator(state.length)) {
let source_articulation_points = state[source_district_idx].filter(point => is_swappable(point_to_district, point, source_district_idx));
for(let source_point_idx of cyclic_generator(source_articulation_points.length)) {
let source_point = source_articulation_points[source_point_idx];
for(let neighbour_idx of cyclic_generator(4)) {
let neighbour = neighbours[neighbour_idx];
let target_district_idx = grid_get(point_to_district, source_point.x + neighbour.x, source_point.y + neighbour.y);
if (_.isUndefined(target_district_idx) || target_district_idx == source_district_idx) {
continue;
}
// swap the source point
point_to_district[source_point.x][source_point.y] = target_district_idx;
_.remove(state[source_district_idx], ({x, y}) => x == source_point.x && y == source_point.y);
// we don't add the point the the target array yet because we don't want to swap that point back
// try to find a point in target_district that we can move to source_district
let target_articulation_points = state[target_district_idx].filter(point => is_swappable(point_to_district, point, target_district_idx));
for(let target_point_idx of cyclic_generator(target_articulation_points.length)) {
let target_point = target_articulation_points[target_point_idx];
for(let n of neighbours) {
if(grid_get(point_to_district, target_point.x + n.x, target_point.y + n.y) === source_district_idx) {
// found a point that we can swap!
// console.log('swapping points!', source_point, target_point);
_.remove(state[target_district_idx], ({x, y}) => x == target_point.x && y == target_point.y);
state[target_district_idx].push(source_point);
state[source_district_idx].push(target_point);
return state;
}
}
}
// unswap source point since we were unsuccessful
point_to_district[source_point.x][source_point.y] = source_district_idx;
state[source_district_idx].push(source_point);
}
}
}
throw 'Could not find any states to swap' // this should never happen, since there will always be the option of reversing the previous step
}
/*
Return whether a point can be removed from a district without creating disjoint districts.
In graph theory, points that cannot be removed are articulation points.
For a general algorithm, see: https://stackoverflow.com/questions/15873153/explanation-of-algorithm-for-finding-articulation-points-or-cut-vertices-of-a-gr
My version takes advantage of the fact that we're on a grid and that all the districts must be continuous,
so we can consider only the immediate neighbours of a point.
*/
function is_swappable(grid, p, district) {
// if the the point is not even in this district, it makes no sense for this to consider this point at all
if(grid[p.x][p.y] != district) {
return false;
}
// if two opposite edges are part of this district, this is an articulation point
// .x. x is an articulation point
// Exception:
// .x. x is not an articulation point
// ...
if (grid_get(grid, p.x+1, p.y) === district && grid_get(grid, p.x-1, p.y) === district && grid_get(grid, p.x, p.y+1) !== district && grid_get(grid, p.x, p.y-1) !== district) {
return false;
}
if (grid_get(grid, p.x, p.y+1) === district && grid_get(grid, p.x, p.y-1) === district && grid_get(grid, p.x+1, p.y) !== district && grid_get(grid, p.x-1, p.y) !== district) {
return false;
}
// check if any corners are missing:
// .x x is not an articulation point .x x is an articulation point
// .. .
for(let i = 0; i < 4; i++) {
let nx = neighbours[i].x;
let ny = neighbours[i].y;
let nx2 = neighbours[(i+1)%4].x;
let ny2 = neighbours[(i+1)%4].y;
if (grid_get(grid, p.x+nx, p.y+ny) === district && grid_get(grid, p.x+nx2, p.y+ny2) === district && grid_get(grid, p.x+nx+nx2, p.y+ny+ny2) !== district) {
return false;
}
}
return true;
}
/** Count how many districts each party wins */
function get_winners(state, voters) {
let red_wins = 0;
let blue_wins = 0;
let tied = 0;
let wasted_red_votes= 0; // see https://en.wikipedia.org/wiki/Wasted_vote
let wasted_blue_votes = 0;
state.forEach(district => {
let counts = _.countBy(district.map(({x, y}) => voters[x][y].vote))
if ((counts[BLUE] || 0) > (counts[RED] || 0)) {
blue_wins++;
wasted_blue_votes += (counts[BLUE] || 0) - VOTERS_PER_DISTRICT / 2 - 1;
wasted_red_votes += (counts[RED] || 0);
}
else if ((counts[RED] || 0) > (counts[BLUE] || 0)) {
red_wins++;
wasted_red_votes += (counts[RED] || 0) - VOTERS_PER_DISTRICT / 2 - 1;
wasted_blue_votes += (counts[BLUE] || 0);
}
else {
tied++;
}
});
return {red_wins, blue_wins, tied, wasted_red_votes, wasted_blue_votes};
}
/* GUI */
/* Display a grid showing which districts each party won */
function render_districts(state, voters) {
let red_districts = 0;
let blue_districts = 0;
let grey_districts = 0;
// Color each district
state.forEach(district => {
let counts = _.countBy(district.map(({x, y}) => voters[x][y].vote))
let district_color;
if ((counts[BLUE] || 0) > (counts[RED] || 0)) {
district_color = 'blue' + blue_districts++;
}
else if ((counts[RED] || 0) > (counts[BLUE] || 0)) {
district_color = 'red' + red_districts++;
}
else {
district_color = 'grey' + grey_districts++;
}
district.map(({x, y}) => voters[x][y].district_color = district_color);
});
return m('table', [
m('tbody', voters.map(row =>
m('tr', row.map(cell => m('td', {'class': cell.district_color}, cell.vote)))
))
]);
}
/** Score a state with four criteria:
- maximize number of red districts
- minimize number of blue districts
- minimize number of red voters in districts that red wins
- maximize number of blue voters in districts that blue wins
The first two criteria are arbitrarily worth 10x more than the latter two
The latter two are to nudge the final result toward the correct solution
*/
function heuristic(state) {
let {red_wins, blue_wins, tied, wasted_red_votes, wasted_blue_votes} = get_winners(state, voters);
return red_wins - blue_wins - 0.1 * wasted_red_votes + 0.1 * wasted_blue_votes;
}
/**
Optimization routine to find the maximum of prob_fcn.
prob_fcn: function to maximize. should take state as its argument
transition: how to generate another state from the previous state
initialize_state: a function that returns an initial state
iters: number of iterations to run
Stolen from my repo here: https://github.com/c2huc2hu/automated-cryptanalysis/blob/master/part3.js
*/
function anneal(prob_fcn, transition, initialize_state, seeds=1, iters=1000) {
let best_result = initialize_state();
for(let i=0; i<seeds; i++) {
let curr_state = initialize_state();
let curr_cost = prob_fcn(curr_state);
// perform annealing. do a few extra steps with temp=0 to refine the final solution
for(let j=0; j<iters; j++) {
let candidate_state = transition(curr_state);
let candidate_cost = prob_fcn(candidate_state);
temp = 0.8 - j / iters;
if(candidate_cost >= curr_cost || Math.random() < temp) {
curr_state = candidate_state;
curr_cost = candidate_cost;
}
}
if(prob_fcn(curr_state) > prob_fcn(best_result)) {
best_result = curr_state;
}
}
return best_result;
}
let voters = generate_voters();
let state = generate_initial_state();
// main rendering code: this code renders the UI
m.mount(document.getElementById('actions'), {view: function() {
return m('div', [
m('button', {onclick: () => state = generate_initial_state()}, 'Reset'),
m('button', {onclick: () => state = evolve_state(state)}, 'Randomly evolve'), // randomly evolves
m('br'),
m('label', {'for': 'radio-blue'}, 'Gerrymander for blue'),
m('input', {type: 'radio', name: 'heuristic', value: 'blue', id: 'radio-blue'}),
m('label', {'for': 'radio-red'}, 'Gerrymander for red'),
m('input', {type: 'radio', name: 'heuristic', value: 'red', id: 'radio-red'}),
m('br'),
m('label', {'for': 'anneal-steps'}, 'Anneal steps: '),
m('input', {id: 'anneal-steps', type: 'number', value: '500'}),
m('button', {onclick: function() {
let minimize = document.getElementById('radio-red').checked;
let _heuristic = minimize ? heuristic : state => -heuristic(state)
let new_state = anneal(_heuristic, evolve_state, generate_initial_state, 1, parseInt(document.getElementById('anneal-steps').value));
if(_heuristic(new_state) > _heuristic(state)) {
state = new_state;
}
else {
console.log('found no better solutions')
}
}}, 'Anneal!'),
]);
}});
// This renders the grid
m.mount(document.getElementById('grid'), {
view: function() {
return render_districts(state, voters)
}
});
// state = anneal(heuristic, evolve_state, generate_initial_state, 5, 1000);
document.getElementById('radio-red').checked = true;
m.redraw();
/* Layout */
table {
border: solid 1px black;
}
td {
padding: 5px;
border: solid 1px black;
}
button {
margin: 10px;
}
p {
max-width: 500px;
}
/* Colour classes. In hindsight, this wasn't a good idea */
.red0 {
background-color: red;
}
.red1 {
background-color: darkred;
}
.red2 {
background-color: pink;
}
.red3 {
background-color: deeppink;
}
.red4 {
background-color: lightsalmon;
}
.blue0 {
background-color: aqua;
}
.blue1 {
background-color: cadetblue;
}
.blue2 {
background-color: steelblue;
}
.blue3 {
background-color: royalblue;
}
.blue4 {
background-color: midnightblue;
}
.grey0 {
background-color: lightgrey;
}
.grey1 {
background-color: silver;
}
.grey2 {
background-color: darkgray;
}
.grey3 {
background-color: gray;
}
.grey4 {
background-color: dimgray;
}
<!DOCTYPE html>
<html>
<head>
<script data-require="lodash.js@4.17.4" data-semver="4.17.4" src="https://cdn.jsdelivr.net/npm/lodash@4.17.4/lodash.min.js"></script>
<script data-require="mithril@1.0.1" data-semver="1.0.1" src="https://cdnjs.cloudflare.com/ajax/libs/mithril/1.0.1/mithril.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Gerrymandering simulation</h1>
<p>
There are two parties, red and blue (chosen because they contrast well).
Each person will always vote a certain way, and is marked with R or B in the table.
People are divided into districts, shown here as groups of people marked in a single colour.
</p>
<p>
Use the buttons below to divide up districts.
The reset button will restore the initial state.
The randomly-evolve button will swap two people between districts
The anneal button will optimize for your chosen party.
You should limit the number of steps to ~1000 or your browser will appear to hang.
In general, it is sufficient to run a few seeds for 500 iterations.
</p>
<div id="grid"></div>
<div id="actions"></div>
<script src="script.js"></script>
</body>
</html>