背景
编程农场又史低了,这个游戏在我愿望清单里面待挺久了,因为价格和其他原因,我一直没有购买,于是趁着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;
}