编程农场——入坑游玩第一天

背景

编程农场又史低了,这个游戏在我愿望清单里面待挺久了,因为价格和其他原因,我一直没有购买,于是趁着steam编程游戏节,我直接买下了这款有意思的游戏。

游戏玩法

在购买这款游戏前,我就对这个游戏有所耳闻,这个游戏就是通过编写代码,驱动一个无人机实行自动化收割、种植等功能,而这种自动化生产链游戏,游戏的趣味就在于,通过自己的方式,最大化产线生成效率。

初期思路

这种最大化产线效率显然可以通过编程的方式完成,我们可以写一个程序完成这件事

第一次玩,还没有解锁太多作物,我想着先生产基础的资源,把初期要用到的东西都解锁了,并且防止后面基础资源短缺,于是,我想这先生产干草、木材和胡萝卜,我们需要将这三者的单位时间产量最大化,同时可以保证收支平衡,因为种植胡萝卜需要消耗其他两种作物,我的第一个简单粗暴的想法就是,枚举所有作物分布,同时对任意作物分布找到最优效率路径,但是在实现的过程中,我偷偷计算了一下复杂度,发现哪怕我进行了简化的操作,可能的作物分布依旧达到了可怕的 \(3^{n^2}\) ,这意味着使用暴力,会直接爆炸,同时,从复杂度,也可以看出这是一个np难问题,因此我们要采用一些启发性的方法来求最优解,对于启发式算法,我最擅长的就是模拟退火了,于是我就想使用SA来解决这个问题,模拟退火需要一个清晰的,对于最优解的评估方法,我们所求的最优解是三个产物的产出效率最高,但是因为这个状态考虑起来过于复杂,于是我在开始时想着,因为无人机飞行消耗时间较小,能不能先不考虑飞行的时序,只考虑图中有多少数量的不同作物,即每种作物各占多少格,这样可以大幅减少目标函数的构造难度,我直接统计了田中不同种类作物的数量,然后计算出他们单位时间产出作物数量并求和,但是我在完成之后,发现,如果不考虑无人机的飞行时间和顺序的话,所需要计算的部分全部都是简单的线性计算,那么我们其实可以直接通过线性规划的方式求出其最优解,但是因为写都写了,删除重构是不可能的,于是我决定先将假的SA写好,然后再把目标函数粘贴到另一个副本里面,然后再在已经完成的SA基础上,把目标函数改成考虑时序和移动的做法。

第一版代码

cpp 复制代码
//Simple Version
#include <bits/stdc++.h>
using namespace std;
const double delta = 0.9112;
int n,t;
int v;
int t_move,t_harvest,t_plant,t_till; //移动耗时,收割耗时,种植耗时,翻地耗时
int g_grass,g_bush,g_corrot; //成熟耗时
int num_hay,num_wood,num_corrot; //作物数量
int perharvest_grass,perharvest_bush,perharvest_corrot; //单次收获量
int nowx,nowy; //当前无人机位置
int mp[2000][2000]; //作物分布图
int tmp_mp[2002][2002];
bool vis[2002][2002];
int k0;
double ans = -1e9;
int best_mp[2000][2000]; //最优分布
double best = -1e18;
inline void init(){
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			mp[i][j] = 1;
		}
	}
}
//Dont consider moving spend
//SA
inline double cal(){
	int cnt1 = 0,cnt2 = 0,cnt3 = 0; //cnt1: hay cnt2: wood cnt3: corrot
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			if (tmp_mp[i][j] == 1){
				cnt1++;
			}
			else if (tmp_mp[i][j] == 2){
				cnt2++;
			}
			else if (tmp_mp[i][j] == 3){
				cnt3++;
			}
		}
	}
	int t1 = t_harvest+g_grass; //草全流程耗时
	int t2 = t_harvest+t_plant+g_bush; //灌木全流程耗时
	int t3 = t_harvest+t_plant+t_till+g_corrot; //胡萝卜全流程耗时
	int num1 = perharvest_grass*cnt1; //草收获量
	int num2 = perharvest_bush*cnt2; //灌木收获量
	int num3 = perharvest_corrot*cnt3; //胡萝卜收获量
	double u1 = 1.0*num1/t1 - 1.0*cnt3/t3; //单位时间草收获量
	double u2 = 1.0*num2/t2 - 1.0*cnt3/t3; //单位时间灌木收获量
	double u3 = 1.0*num3/t3; //单位时间胡萝卜收获量
	
	if (u1 < 0 or u2 < 0){
		return -1;
	}
	
	//权重
	// double w1 = 1.0*t1/perharvest_grass;
	// double w2 = 1.0*t2/perharvest_bush;
	// double w3 = 1.0*t3/perharvest_corrot;
	double sum = u1 + u2 + u3;
	return sum;
}
inline void SA(){
	k0 = n*n;
	double T = 6000;
	double T0 = T;
	while (T > 1e-8){
		int k = max(1,(int)(k0*(T/T0)));
		vector<tuple<int,int,int>> tmp; //扰动点
		memset(vis,0,sizeof(vis));
		for (int i=1;i<=k;i++){
			int tmp_i = rand()%n+1,tmp_j = rand()%n+1;
			int crop_type = rand()%3+1;
			while (true){
				if (vis[tmp_i][tmp_j] == 0 and mp[tmp_i][tmp_j] != crop_type){
					tmp.push_back({tmp_i,tmp_j,crop_type});
					vis[tmp_i][tmp_j] = true;
					break;
				}
				else if (vis[tmp_i][tmp_j] == 0 and mp[tmp_i][tmp_j] == crop_type){
					crop_type = rand()%3+1;
				}
				else {
					tmp_i = rand()%n+1,tmp_j = rand()%n+1;
					crop_type = rand()%3+1;
				}
			}
		}
		for (int i=1;i<=n;i++){
			for (int j=1;j<=n;j++){
				tmp_mp[i][j] = mp[i][j];
			}
		}
		for (auto [i,j,type] : tmp){
			tmp_mp[i][j] = type;
		}
		double now = cal();
		double Delta = now-ans;
		if (Delta >= 0 or exp(Delta/T) > 1.0*rand()/RAND_MAX){
			ans = now;
			for (int i=1;i<=n;i++){
				for (int j=1;j<=n;j++){
					mp[i][j] = tmp_mp[i][j];
				}
			}
			if (ans > best){
				best = ans;
				memcpy(best_mp,mp,sizeof(mp));
			}
		}
		T *= delta;
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>t;
	cin>>v;
	cin>>t_move>>t_harvest>>t_plant>>t_till;
	cin>>g_grass>>g_bush>>g_corrot;
	cin>>num_hay>>num_wood>>num_corrot;
	cin>>perharvest_grass>>perharvest_bush>>perharvest_corrot;
	cin>>nowx>>nowy;
	//3^(n*n) NP-hard?
	init();
	int times = 100;
	while (times--){
		SA();
	}
	memcpy(mp,best_mp,sizeof(mp));
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			cout<<mp[i][j]<<' ';
		}
		cout<<endl;
	}
	return 0;
}

显然在仅考虑不同种类作物数量的情况下,这一方法不是最优的,应该采用线性规划的方式求解最优解。

第二版代码-基于线性规划

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
int n,t;
int v;
int t_move,t_harvest,t_plant,t_till;
int g_grass,g_bush,g_corrot;
int num_hay,num_wood,num_corrot;
int perharvest_grass,perharvest_bush,perharvest_corrot;
int nowx,nowy;
int mp[2000][2000];
inline void solve(){
	int t1 = t_harvest+g_grass; //草全流程耗时
	int t2 = t_harvest+t_plant+g_bush; //灌木全流程耗时
	int t3 = t_harvest+t_plant+t_till+g_corrot; //胡萝卜全流程耗时
	
	double a1 = 1.0*perharvest_grass/t1;
	double a2 = 1.0*perharvest_bush/t2;
	double a3 = 1.0*perharvest_corrot/t3;
	double k = 1.0/t3;
	
	// int num1 = perharvest_grass*cnt1; //草收获量
	// int num2 = perharvest_bush*cnt2; //灌木收获量
	// int num3 = perharvest_corrot*cnt3; //胡萝卜收获量
	// double u1 = 1.0*num1/t1 - 1.0*cnt3/t3; //单位时间草收获量
	// double u2 = 1.0*num2/t2 - 1.0*cnt3/t3; //单位时间灌木收获量
	// double u3 = 1.0*num3/t3; //单位时间胡萝卜收获量
	
	int num = pow(n,2);
	double best_sum = -1e9;
	int ans1 = 0,ans2 = 0,ans3 = 0;
	for (int c3=0;c3<=num;c3++){
		int R = num-c3;
		int tmp1 = (int)ceil(k*c3/a1 - 1e-9);
		if (tmp1 < 0){
			tmp1 = 0;
		}
		int tmp2 = (int)ceil(k*c3/a2 - 1e-9);
		if (tmp2 < 0){
			tmp2 = 0;
		}
		if (tmp1 + tmp2 > R) continue;
		int c1 = 0,c2 = 0;
		if (a1 >= a2){
			c2 = tmp2;
			c1 = R - tmp2;
			if (c1 < tmp1) continue;
		}
		else{
			c1 = tmp1;
			c2 = R - tmp1;
			if (c2 < tmp2) continue;
		}
		double s = c1*a1 + c2*a2 + (a3-2.0*k)*c3;
		if (s > best_sum){
			best_sum = s;
			ans1 = c1;
			ans2 = c2;
			ans3 = c3;
		}
	}
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			if (ans1 != 0){
				mp[i][j] = 1;
				ans1--;
			}
			else if (ans2 != 0){
				ans2--;
				mp[i][j] = 2;
			}
			else if (ans3 != 0){
				ans3--;
				mp[i][j] = 3;
			}
		}
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>t;
	cin>>v;
	cin>>t_move>>t_harvest>>t_plant>>t_till;
	cin>>g_grass>>g_bush>>g_corrot;
	cin>>num_hay>>num_wood>>num_corrot;
	cin>>perharvest_grass>>perharvest_bush>>perharvest_corrot;
	cin>>nowx>>nowy;
	solve();
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			cout<<mp[i][j]<<' ';
		}
		cout<<endl;
	}
	return 0;
}

这样我们就得到了基于线性规划的最优解求解代码了,不过这个游戏里面,有一个因素也影响着效率,只不过之前简化时忽略了而已,这个因素就是无人机移动消耗的时间、无人机操作的时间还有时序问题,这个因素其实在游戏中是比较重要的,因为只有一架无人机,移动操作时消耗的时间确实会很多,而如果导入了这个因素,那么这版线性规划的代码就难以解决了。

第三版代码-添加移动以及时序处理功能的SA

为了解决之前提到的问题,我们又回到了启发性算法上,我们需要重新设计一下SA的目标函数,让他能够处理时序问题,我们可以注意到,无人机对于每个操作的时间是固定的,并且我们只有一台无人机,因此我们可以通过离散化的方式处理时序问题,由于一下子就考虑时序的优化过于困难,我们可以先假定无人机按照固定周期遍历全图,累加全局时间,逐格判断成熟并执行操作,最后统计净产出。

1. 核心思路

  • 外层:SA 优化农田布局。每个格子有一个"期望状态",包括地面类型(草地/土壤)和作物类型(空/草/灌木/胡萝卜)。
  • 内层 :给定布局,无人机按固定路线(例如蛇形)循环遍历全图。每移动一格、每执行一个动作,全局时间 t 都增加对应耗时。
  • 时序 :每个格子记录 planted_at,成熟时间 = planted_at + g_作物。无人机到达格子时,用当前 t 判断是否成熟:
    • 成熟 → 收割,增加库存;然后按该格子的"期望作物"立即补种(若库存足够且地面合适)。
    • 不成熟 → 跳过,继续前往下一格。
  • 目标函数:模拟足够长时间后,统计三种资源的净增量(产出 − 消耗),加权求和后除以测量时长,得到单位时间净收益。SA 以此作为适应度。

我想着最简单的遍历方式就是蛇形遍历了,按理来说这种遍历方式也是遍历效率最高的,因为无人机只支持单步移动,不支持跳步,因此我们将先固定以蛇形遍历为遍历顺序,然后通过刚刚设计的简单时序状态机,完成处理,实际上,设计出来之后,实现其实没有想象中那么困难,其实就是改一改第一版的代码罢了。

cpp 复制代码
//Simple Version
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const double delta = 0.9112;
int n,t;
int v;
int t_move,t_harvest,t_plant,t_till; //移动耗时,收割耗时,种植耗时,翻地耗时
int g_grass,g_bush,g_corrot; //成熟耗时
int num_hay,num_wood,num_corrot; //作物数量
int perharvest_grass,perharvest_bush,perharvest_corrot; //单次收获量
int nowx,nowy; //当前无人机位置
int mp[2000][2000]; //作物分布图
int tmp_mp[2002][2002];
bool vis[2002][2002];
int k0;
double ans = -1e9;
int best_mp[2000][2000]; //最优分布
ll time_mp[2000][2000]; //各格时间
double best = -1e18;
inline void init(){
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			mp[i][j] = 1;
		}
	}
}
//Dont consider moving spend
//SA
inline double cal(){
	memset(time_mp,0,sizeof(time_mp));
	int cnt1 = 0,cnt2 = 0,cnt3 = 0; //cnt1: hay cnt2: wood cnt3: corrot
	//设所有作物已经在土地上种上,但是还没有生长
	ll global_time = 0; //全局时间
	int t1 = t_harvest; //草全流程耗时
	int t2 = t_harvest+t_plant; //灌木全流程耗时
	int t3 = t_harvest+t_plant+t_till; //胡萝卜全流程耗时
	for (int k=1;k<=10;k++){
		for (int i=1;i<=n;i++){
			if (i%2 == 0){
				for (int j=n;j>=1;j--){
					if (mp[i][j] == 1){
						ll delta_time = global_time-time_mp[i][j];
						if (delta_time >= g_grass){
							cnt1++;
							global_time += t1;
							time_mp[i][j] = global_time;
						}
					}
					else if (mp[i][j] == 2){
						ll delta_time = global_time-time_mp[i][j];
						if (delta_time >= g_bush){
							cnt2++;
							global_time += t2;
							time_mp[i][j] = global_time;
						}
					}
					else if (mp[i][j] == 3){
						ll delta_time = global_time-time_mp[i][j];
						if (delta_time >= g_corrot){
							cnt3++;
							global_time += t3;
							time_mp[i][j] = global_time;
						}
					}
					global_time += t_move;
				}
			}
			else{
				for (int j=1;j<=n;j++){
					if (mp[i][j] == 1){
						ll delta_time = global_time-time_mp[i][j];
						if (delta_time >= g_grass){
							cnt1++;
							global_time += t1;
							time_mp[i][j] = global_time;
						}
					}
					else if (mp[i][j] == 2){
						ll delta_time = global_time-time_mp[i][j];
						if (delta_time >= g_bush){
							cnt2++;
							global_time += t2;
							time_mp[i][j] = global_time;
						}
					}
					else if (mp[i][j] == 3){
						ll delta_time = global_time-time_mp[i][j];
						if (delta_time >= g_corrot){
							cnt3++;
							global_time += t3;
							time_mp[i][j] = global_time;
						}
					}
					global_time += t_move;
				}
			}
		}
	}
	ll num1 = 1LL*perharvest_grass*cnt1; //草收获量
	ll num2 = 1LL*perharvest_bush*cnt2; //灌木收获量
	ll num3 = 1LL*perharvest_corrot*cnt3; //胡萝卜收获量
	double u1 = 1.0*num1/global_time - 1.0*cnt3/global_time; //单位时间草收获量
	double u2 = 1.0*num2/global_time - 1.0*cnt3/global_time; //单位时间灌木收获量
	double u3 = 1.0*num3/global_time; //单位时间胡萝卜收获量
	
	if (u1 < 0 or u2 < 0){
		return -1e18;
	}
	
	//权重
	// double w1 = 1.0*t1/perharvest_grass;
	// double w2 = 1.0*t2/perharvest_bush;
	// double w3 = 1.0*t3/perharvest_corrot;
	double sum = u1 + u2 + u3;
	return sum;
}
inline void SA(){
	k0 = n*n;
	double T = 6000;
	double T0 = T;
	while (T > 1e-8){
		int k = max(1,(int)(k0*(T/T0)));
		vector<tuple<int,int,int>> tmp; //扰动点
		memset(vis,0,sizeof(vis));
		for (int i=1;i<=k;i++){
			int tmp_i = rand()%n+1,tmp_j = rand()%n+1;
			int crop_type = rand()%3+1;
			while (true){
				if (vis[tmp_i][tmp_j] == 0 and mp[tmp_i][tmp_j] != crop_type){
					tmp.push_back({tmp_i,tmp_j,crop_type});
					vis[tmp_i][tmp_j] = true;
					break;
				}
				else if (vis[tmp_i][tmp_j] == 0 and mp[tmp_i][tmp_j] == crop_type){
					crop_type = rand()%3+1;
				}
				else {
					tmp_i = rand()%n+1,tmp_j = rand()%n+1;
					crop_type = rand()%3+1;
				}
			}
		}
		for (int i=1;i<=n;i++){
			for (int j=1;j<=n;j++){
				tmp_mp[i][j] = mp[i][j];
			}
		}
		for (auto [i,j,type] : tmp){
			tmp_mp[i][j] = type;
		}
		double now = cal();
		double Delta = now-ans;
		if (Delta >= 0 or exp(Delta/T) > 1.0*rand()/RAND_MAX){
			ans = now;
			for (int i=1;i<=n;i++){
				for (int j=1;j<=n;j++){
					mp[i][j] = tmp_mp[i][j];
				}
			}
			if (ans > best){
				best = ans;
				memcpy(best_mp,mp,sizeof(mp));
			}
		}
		T *= delta;
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>t;
	cin>>v;
	cin>>t_move>>t_harvest>>t_plant>>t_till;
	cin>>g_grass>>g_bush>>g_corrot;
	cin>>num_hay>>num_wood>>num_corrot;
	cin>>perharvest_grass>>perharvest_bush>>perharvest_corrot;
	cin>>nowx>>nowy;
	//3^(n*n) NP-hard?
	init();
	int times = 100;
	while (times--){
		SA();
	}
	memcpy(mp,best_mp,sizeof(mp));
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			cout<<mp[i][j]<<' ';
		}
		cout<<endl;
	}
	return 0;
}

实际上机时,我才用了python优化了我的输出,让布局和数据更加的可读

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
读取 layout.csv 和 meta.txt,生成带标题、图例、统计的布局图。
"""

import os
import sys
import numpy as np
import matplotlib
matplotlib.use("Agg")  # 无 GUI 后端,适合被 C++ 调用
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from matplotlib.patches import Patch

# 中文字体(按系统可用字体依次尝试)
plt.rcParams["font.sans-serif"] = [
    "SimHei", "Microsoft YaHei", "WenQuanYi Zen Hei", "DejaVu Sans"
]
plt.rcParams["axes.unicode_minus"] = False

# 0=空, 1=草, 2=灌木, 3=胡萝卜
COLORS = ["#f0f0f0", "#7ec850", "#8b5a2b", "#ff8c00"]
LABELS = ["空", "草", "灌木", "胡萝卜"]
CMAP = ListedColormap(COLORS)


def main():
    if not os.path.exists("layout.csv"):
        print("错误:找不到 layout.csv", file=sys.stderr)
        sys.exit(1)

    layout = np.loadtxt("layout.csv", delimiter=",", dtype=int)
    n = layout.shape[0]

    # 读取 meta.txt
    meta = {}
    if os.path.exists("meta.txt"):
        with open("meta.txt", "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if "=" in line:
                    k, v = line.split("=", 1)
                    meta[k] = v

    best_score = float(meta.get("best_score", 0))

    # 根据 n 决定画布尺寸和是否画网格
    if n <= 30:
        figsize = (14, 7)
        show_grid = True
    elif n <= 100:
        figsize = (16, 8)
        show_grid = False
    else:
        figsize = (18, 9)
        show_grid = False

    fig = plt.figure(figsize=figsize)

    # ===== 左图:布局 =====
    ax1 = fig.add_subplot(1, 2, 1)
    ax1.imshow(layout, cmap=CMAP, vmin=0, vmax=3, interpolation="nearest")
    ax1.set_xticks([])
    ax1.set_yticks([])
    ax1.set_title(f"农田布局 (n={n})", fontsize=13)

    if show_grid:
        ax1.set_xticks(np.arange(-0.5, n, 1), minor=True)
        ax1.set_yticks(np.arange(-0.5, n, 1), minor=True)
        ax1.grid(which="minor", color="gray", linewidth=0.3)
        ax1.tick_params(which="minor", bottom=False, left=False)

    legend_elements = [
        Patch(facecolor=COLORS[i], edgecolor="gray", label=LABELS[i])
        for i in range(4)
    ]
    ax1.legend(
        handles=legend_elements, loc="upper right",
        bbox_to_anchor=(1.15, 1), fontsize=10
    )

    # ===== 右图:作物占比饼图 =====
    ax2 = fig.add_subplot(1, 2, 2)
    counts = [int(np.sum(layout == i)) for i in range(4)]
    total = sum(counts)

    non_zero = [
        (c, LABELS[i], COLORS[i])
        for i, c in enumerate(counts) if c > 0
    ]
    if non_zero:
        sizes = [x[0] for x in non_zero]
        labels = [
            f"{x[1]}\n{x[0]}格 ({x[0] / total * 100:.1f}%)"
            for x in non_zero
        ]
        colors = [x[2] for x in non_zero]
        ax2.pie(
            sizes, labels=labels, colors=colors,
            autopct="%1.1f%%", startangle=90,
            textprops={"fontsize": 10}
        )
        ax2.set_title("作物占比", fontsize=13)

    # ===== 总标题 =====
    param_info = (
        f"v={meta.get('v','?')}  实际耗时: "
        f"移动={meta.get('e_move','?')} 收割={meta.get('e_harvest','?')} "
        f"种植={meta.get('e_plant','?')} 翻地={meta.get('e_till','?')}\n"
        f"成熟: 草={meta.get('g_grass','?')} 灌木={meta.get('g_bush','?')} "
        f"胡萝卜={meta.get('g_corrot','?')}  单次收获: "
        f"{meta.get('perharvest_grass','?')}/"
        f"{meta.get('perharvest_bush','?')}/"
        f"{meta.get('perharvest_corrot','?')}"
    )
    plt.suptitle(
        f"适应度 = {best_score:.4f}\n{param_info}",
        fontsize=12
    )
    plt.tight_layout(rect=[0, 0, 1, 0.95])

    out_path = "layout.png"
    plt.savefig(out_path, dpi=200, bbox_inches="tight")
    plt.close(fig)
    print(f"已保存 {out_path}")


if __name__ == "__main__":
    main()

然而,在测试过程中,我发现了一个严重的问题,我的目标函数,仅是无权重的求和,但是这么做,会导致全局的分布退化到全为草,这显然是我们不想见到的,因此,为了保证三种作物均存在,我们需要为sum设置权重,又或者,我们不采用求和作为目标函数的返回值,而是采用其他计算方法处理得到的。

这是参考ai给出的方案

方案 公式 优点 缺点 适用场景
等权线性 \(u_1+u_2+u_3\) 简单 退化为全草 不推荐
加权线性 \(w_1u_1+w_2u_2+w_3u_3\) 可调偏好 权重要试 有明确价值比
约束 + 胡萝卜 \(\max u_3,\ u_1\ge0,\ u_2\ge0\) 符合游戏目标 胡萝卜可能过多 初期目标
约束 + 安全余量 \(\max u_3,\ u_1\ge\varepsilon,u_2\ge\varepsilon\) 留缓冲 \(\varepsilon\) 要试 稳健版
几何平均 \((u_1u_2u_3)^{\frac{1}{3}}\) 强制均衡 收敛慢 三者同等重要
调和平均 \(\frac{3}{\frac{1}{u_1}+\frac{1}{u_2}+\frac{1}{u_3}}\) 对短板最敏感 可能被短板拖死 严格均衡
最小值 \((u_1+u_2+u_3)\) 最严格均衡 不可导,SA 难收敛 不推荐

第四版代码-优化数值计算方法

cpp 复制代码
//Simple Version (fixed)
//  - cal() 改为接受布局参数的纯函数:评估 tmp_mp 而不是 mp
//  - ans 始终等于 cal(mp),避免 best_mp 记录了从未评估的状态
//  - 自适应初始温度:避免 T0=6000 在适应度 ~0.3 时几乎全接受导致随机游走
//  - 用 cur_tag 取代每次 memset(vis, ...),只清用到的 [1..n] 子阵以提速
//  - 随机数换成 mt19937 并按时钟播种;设 SA_SEED 环境变量可固定种子复现
//  - 接上速度因子 v:所有动作耗时按 Δt = ceil(基础耗时 / v) 折算
#include <bits/stdc++.h>
#define ll long long
using namespace std;

// ===== 随机数 =====
// 用 mt19937 取代 rand():MSVC 的 RAND_MAX 只有 32767,一轮 SA 要消耗约 37 万次
// 随机数,LCG 的周期和低比特质量都不够用。
// 默认用高精度时钟播种(每次运行结果不同);需要复现时设环境变量 SA_SEED=<整数>。
static mt19937 rng;
inline int rnd_int(int lo, int hi){ return uniform_int_distribution<int>(lo, hi)(rng); }
inline double rnd01(){ return uniform_real_distribution<double>(0.0, 1.0)(rng); }

const double delta = 0.9112;
const int MAXN = 2002;        // 数组宽度(下标 1..n 有效,留出 [MAXN][MAXN] 的余地)
const int ROUNDS = 10;        // cal 内部蛇形遍历的轮数,越大越接近稳态、耗时也线性增加
int n,t;
int v;
int t_move,t_harvest,t_plant,t_till; //基础耗时(输入原值)
// 折算后的实际耗时:题目规定每个动作 Δt = ceil(基础耗时 / v)
int e_move,e_harvest,e_plant,e_till;
int g_grass,g_bush,g_corrot; //成熟耗时
int num_hay,num_wood,num_corrot; //作物数量
int perharvest_grass,perharvest_bush,perharvest_corrot; //单次收获量
int nowx,nowy; //当前无人机位置
int mp[MAXN][MAXN];           //当前解
int tmp_mp[MAXN][MAXN];       //候选解
int best_mp[MAXN][MAXN];      //历史最优解
ll time_mp[MAXN][MAXN];       //cal 用:各格上次被操作时间(仅 [1..n] 范围)
int vis_tag[MAXN][MAXN];      //扰动去重戳记
int cur_tag = 0;
int k0;
double ans = -1e9;            // 当前解 mp 的适应度,每轮 SA 开头会被校准为 cal(mp)
double best = -1e18;
unsigned int g_seed = 0;      // 本轮实际使用的随机种子,写进 meta.txt 便于复现
inline void init(){
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			mp[i][j] = 1;
		}
	}
}

// Δt = ceil(基础耗时 / v),v 至少按 1 处理
inline int eff(int base){
	int vv = (v < 1) ? 1 : v;
	return (base + vv - 1) / vv;
}

// 计算布局 a 的适应度(纯函数:只读 a,不依赖任何全局状态)
// 无人机按蛇形顺序遍历 ROUNDS 轮,逐格判断成熟并执行操作,统计净产出率
inline double cal(int a[MAXN][MAXN]){
	// 只清理 [1..n] 范围的 time_mp,避免对 32MB 整块 memset
	for (int i=1;i<=n;i++){
		memset(time_mp[i]+1, 0, n*sizeof(ll));
	}
	int cnt1 = 0,cnt2 = 0,cnt3 = 0; //cnt1: hay cnt2: wood cnt3: corrot
	ll global_time = 0; //全局时间
	// 单次收获操作的耗时(已按 Δt=ceil(基础/v) 折算;成长时间由下方的"delta >= 成长阈值"判断隐式处理)
	int t1 = e_harvest;                          // 草:收割后原地重生,无需补种
	int t2 = e_harvest+e_plant;                  // 灌木:收割 + 补种
	int t3 = e_harvest+e_plant+e_till;           // 胡萝卜:收割 + 翻地 + 补种
	for (int k=1;k<=ROUNDS;k++){
		for (int i=1;i<=n;i++){
			// 蛇形扫描:奇数行正序、偶数行逆序
			int js, jt, stp;
			if (i%2 == 0){ js = n; jt = 0;  stp = -1; }
			else         { js = 1; jt = n+1; stp =  1; }
			for (int j=js; j!=jt; j+=stp){
				int type = a[i][j];
				ll delta_time = global_time - time_mp[i][j];
				if (type == 1){
					if (delta_time >= g_grass){
						cnt1++;
						global_time += t1;
						time_mp[i][j] = global_time;
					}
				}
				else if (type == 2){
					if (delta_time >= g_bush){
						cnt2++;
						global_time += t2;
						time_mp[i][j] = global_time;
					}
				}
				else if (type == 3){
					if (delta_time >= g_corrot){
						cnt3++;
						global_time += t3;
						time_mp[i][j] = global_time;
					}
				}
				global_time += e_move;
			}
		}
	}
	ll num1 = 1LL*perharvest_grass*cnt1; //草收获量
	ll num2 = 1LL*perharvest_bush*cnt2; //灌木收获量
	ll num3 = 1LL*perharvest_corrot*cnt3; //胡萝卜收获量
	// 每棵胡萝卜种植消耗 1 干草 + 1 木材,故从草/灌木的净产出里各扣除 cnt3
	double u1 = 1.0*num1/global_time - 1.0*cnt3/global_time; //单位时间草净产出
	double u2 = 1.0*num2/global_time - 1.0*cnt3/global_time; //单位时间灌木净产出
	double u3 = 1.0*num3/global_time; //单位时间胡萝卜产出

	if (u1 < 0 or u2 < 0){
		return -1e18; // 入不敷出:草/灌木产量撑不起胡萝卜消耗,方案不可行
	}

//	double sum = 3/(1/u1+1/u2+1/u3);
	double sum = cbrt(u1*u2*u3);
	return sum;
}
inline void SA(){
	k0 = n*n;
	// 把 ans 校准成当前解的真实适应度,避免历史残留造成 best_mp 与分数错位
	ans = cal(mp);

	// 自适应初始温度:采样若干单格扰动,让典型劣化移动在 T0 时的接受率约 0.5
	double T0;
	{
		const int SAMPLES = 30;
		double s = 0;
		for (int q=0; q<SAMPLES; q++){
			int i = rnd_int(1, n), j = rnd_int(1, n);
			int old = mp[i][j], nt = old;
			while (nt == old) nt = rnd_int(1, 3);
			mp[i][j] = nt;
			double v = cal(mp);
			mp[i][j] = old;
			s += fabs(v - ans);
		}
		T0 = max(s/SAMPLES, 1e-6) / log(2.0);
		if (T0 < 1e-4) T0 = 1e-4;
	}
	double T = T0;
	double T_min = max(1e-9, T0 * 1e-6);
	while (T > T_min){
		int k = max(1,(int)(k0*(T/T0)));
		vector<tuple<int,int,int>> tmp; //扰动点
		cur_tag++; // 本温度步的扰动去重戳记(等价于原来的 memset(vis,0,...))
		for (int i=1;i<=k;i++){
			int tmp_i = rnd_int(1, n), tmp_j = rnd_int(1, n);
			int crop_type = rnd_int(1, 3);
			while (true){
				if (vis_tag[tmp_i][tmp_j] != cur_tag and mp[tmp_i][tmp_j] != crop_type){
					tmp.push_back({tmp_i,tmp_j,crop_type});
					vis_tag[tmp_i][tmp_j] = cur_tag;
					break;
				}
				else if (vis_tag[tmp_i][tmp_j] != cur_tag and mp[tmp_i][tmp_j] == crop_type){
					crop_type = rnd_int(1, 3);
				}
				else {
					tmp_i = rnd_int(1, n), tmp_j = rnd_int(1, n);
					crop_type = rnd_int(1, 3);
				}
			}
		}
		// 拷贝当前解到候选解
		for (int i=1;i<=n;i++){
			memcpy(tmp_mp[i]+1, mp[i]+1, n*sizeof(int));
		}
		for (auto [i,j,type] : tmp){
			tmp_mp[i][j] = type;
		}
		double now = cal(tmp_mp); // 评估候选解(关键修复点:原来是 cal(mp),等于没评估扰动)
		double Delta = now-ans;
		if (Delta >= 0 or exp(Delta/T) > rnd01()){
			// 接受:覆盖 mp,并把 ans 同步到新 mp 的真实分数
			for (int i=1;i<=n;i++){
				memcpy(mp[i]+1, tmp_mp[i]+1, n*sizeof(int));
			}
			ans = now;
			if (ans > best){
				best = ans;
				for (int i=1;i<=n;i++){
					memcpy(best_mp[i]+1, mp[i]+1, n*sizeof(int));
				}
			}
		}
		T *= delta;
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>t;
	cin>>v;
	cin>>t_move>>t_harvest>>t_plant>>t_till;
	cin>>g_grass>>g_bush>>g_corrot;
	cin>>num_hay>>num_wood>>num_corrot;
	cin>>perharvest_grass>>perharvest_bush>>perharvest_corrot;
	cin>>nowx>>nowy;
	// 按 Δt = ceil(基础耗时/v) 折算实际耗时(v 是速度因子,见题目)
	e_move    = eff(t_move);
	e_harvest = eff(t_harvest);
	e_plant   = eff(t_plant);
	e_till    = eff(t_till);
	// 播种:默认高精度时钟(每次运行结果不同);想复现某次结果就设 SA_SEED=<整数>
	g_seed = (unsigned)chrono::high_resolution_clock::now().time_since_epoch().count();
	if (const char* env = getenv("SA_SEED")) g_seed = (unsigned)strtoul(env, nullptr, 10);
	rng.seed(g_seed);
	cerr << "seed = " << g_seed << endl;
	//3^(n*n) NP-hard?
	init();
	int times = 100;
	while (times--){
		SA();
	}
	// 还原历史最优到 mp 并写出
	for (int i=1;i<=n;i++){
		memcpy(mp[i]+1, best_mp[i]+1, n*sizeof(int));
	}
	for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            cout << mp[i][j];
            if (j < n) cout << ',';
        }
        cout << '\n';
    }
	ofstream fout("layout.csv");
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            fout << mp[i][j];
            if (j < n) fout << ',';
        }
        fout << '\n';
    }
    fout.close();

    // ========== 输出参数到 meta.txt ==========
    ofstream meta("meta.txt");
    meta << "n=" << n << "\n";
    meta << "best_score=" << best << "\n";
    meta << "seed=" << g_seed << "\n";
    meta << "t_move=" << t_move << "\n";
    meta << "t_harvest=" << t_harvest << "\n";
    meta << "t_plant=" << t_plant << "\n";
    meta << "t_till=" << t_till << "\n";
    meta << "v=" << v << "\n";
    meta << "e_move=" << e_move << "\n";
    meta << "e_harvest=" << e_harvest << "\n";
    meta << "e_plant=" << e_plant << "\n";
    meta << "e_till=" << e_till << "\n";
    meta << "g_grass=" << g_grass << "\n";
    meta << "g_bush=" << g_bush << "\n";
    meta << "g_corrot=" << g_corrot << "\n";
    meta << "perharvest_grass=" << perharvest_grass << "\n";
    meta << "perharvest_bush=" << perharvest_bush << "\n";
    meta << "perharvest_corrot=" << perharvest_corrot << "\n";
    meta.close();

    // ========== 调用 Python 画图 ==========
#ifdef _WIN32
    int ret = system("python plot_layout.py");
#else
    int ret = system("python3 plot_layout.py");
#endif
    if (ret != 0) {
        cerr << "警告:Python 脚本执行失败,请检查 Python 环境和 plot_layout.py" << endl;
    }
	return 0;
}

更进一步的,如果从全草开始操作,那么其实效率还是有点低,而且无法保证解的优异性和稳定性,因此我们可以先从线性规划入手,先求出一个可能的上界,随后进一步用SA做求解,结合的代码就我分析而言有两种,其一,直接按照线性规划的分布图起手,完全随机,其二,线性规划求出每种作物的种植数量,然后随机分布到图上

首先我们需要修改一下线性规划的代码,因为他还是依据原来的无权重求和的处理方式实现的线性规划,这里懒了,直接选择让ai来改

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
int n,t;
int v;
int t_move,t_harvest,t_plant,t_till;
int g_grass,g_bush,g_corrot;
int num_hay,num_wood,num_corrot;
int perharvest_grass,perharvest_bush,perharvest_corrot;
int nowx,nowy;
int mp[2000][2000];
inline void solve(){
	int t1 = t_harvest+g_grass; //草全流程耗时
	int t2 = t_harvest+t_plant+g_bush; //灌木全流程耗时
	int t3 = t_harvest+t_plant+t_till+g_corrot; //胡萝卜全流程耗时
	
	double a1 = 1.0*perharvest_grass/t1;
	double a2 = 1.0*perharvest_bush/t2;
	double a3 = 1.0*perharvest_corrot/t3;
	double k = 1.0/t3;
	
	// 三项单位时间净产出:
	//   u1 = c1*a1 - k*c3   草净产出(每棵胡萝卜消耗 1 干草)
	//   u2 = c2*a2 - k*c3   灌木净产出(每棵胡萝卜消耗 1 木材)
	//   u3 = c3*a3          胡萝卜产出
	//
	// 评估方法已从「算术和 u1+u2+u3」改为「几何平均 (u1*u2*u3)^(1/3)」。
	// 原算术和版本(保留备查):
	//   double s = c1*a1 + c2*a2 + (a3-2.0*k)*c3;   // 即 u1+u2+u3
	// 注意:算术和是 c1,c2 的线性函数,贪心分配(把余量全给产率高的那种)就是最优;
	// 但几何平均会被最小的那一项拖死,贪心会把 u2 压到 0 导致乘积为 0,
	// 因此下面的分配改成了解析求出的平衡点。
	
	int num = pow(n,2);
	double best_sum = -1e9;   // 目标值 = (u1*u2*u3)^(1/3)
	int ans1 = 0,ans2 = 0,ans3 = 0;
	for (int c3=0;c3<=num;c3++){
		int R = num-c3;          // 分给草/灌木的格子
		double K = k*c3;         // 胡萝卜对草、灌木的消耗速率
		double u3 = c3*a3;
		if (u3 <= 0 or a1 <= 0 or a2 <= 0) continue; // 几何平均要求三项都为正
		
		// u1>0 且 u2>0 对应的 c1 整数可行区间
		int lo = (int)floor(K/a1) + 1;       // c1*a1 > K
		int hi = R - (int)floor(K/a2) - 1;   // (R-c1)*a2 > K
		if (lo < 0) lo = 0;
		if (hi > R) hi = R;
		if (lo > hi) continue;
		
		// 固定 c3 时 u3 是常数,只需最大化 u1*u2。
		// u1*u2 = (c1*a1-K)*((R-c1)*a2-K) 是 c1 的开口向下抛物线,
		// 顶点 x* = R/2 + K*(a2-a1)/(2*a1*a2),整数最优必在 floor/ceil 处。
		double xstar = R/2.0 + K*(a2-a1)/(2.0*a1*a2);
		int cand[2] = {(int)floor(xstar), (int)ceil(xstar)};
		for (int q=0;q<2;q++){
			int c1 = cand[q];
			if (c1 < lo) c1 = lo;
			if (c1 > hi) c1 = hi;
			int c2 = R-c1;
			double u1 = c1*a1 - K;
			double u2 = c2*a2 - K;
			if (u1 <= 0 or u2 <= 0) continue;
			double s = cbrt(u1*u2*u3);   // 几何平均
			if (s > best_sum){
				best_sum = s;
				ans1 = c1;
				ans2 = c2;
				ans3 = c3;
			}
		}
	}
	if (best_sum <= -1e8){
		cerr << "警告:几何平均下无可行解(格子太少,放不下三种作物且都为正产出)" << endl;
	}
	cerr << "geometric mean = " << best_sum
	     << "   (grass/bush/carrot = " << ans1 << "/" << ans2 << "/" << ans3 << ")" << endl;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			if (ans1 != 0){
				mp[i][j] = 1;
				ans1--;
			}
			else if (ans2 != 0){
				ans2--;
				mp[i][j] = 2;
			}
			else if (ans3 != 0){
				ans3--;
				mp[i][j] = 3;
			}
		}
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>t;
	cin>>v;
	cin>>t_move>>t_harvest>>t_plant>>t_till;
	cin>>g_grass>>g_bush>>g_corrot;
	cin>>num_hay>>num_wood>>num_corrot;
	cin>>perharvest_grass>>perharvest_bush>>perharvest_corrot;
	cin>>nowx>>nowy;
	solve();
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			cout<<mp[i][j]<<' ';
		}
		cout<<endl;
	}
	return 0;
}