题目大意:
给你2*n的格子,每个格子上有一个权值。起点在左上角,终点在右下角,只能向右或者向下走。让你重新排列这些权值,使得从起点到终点的路径权值的最大值最小。(路径权值为路径经过的格子权值的累加)权值 a ≤ 5 e 4 , n ≤ 25 a\le 5e4, n\le 25 a≤5e4,n≤25
解题思路:
首先如果假设第一排已经排列好,其中一对相邻的格子权值为a和b,a > b且a在b左边。
那么所有路径中,经过a和b的情况只有三种:
- 都不经过
- 只经过a
- a和b都经过
那么这个时候交换a和b的权值显然不会让最大值变大(一些路径权值不变,一些路径权值变小)
因此,如果排列在第一排的元素确定了,那么它们的权值应该是从左到右递增的。
同样的方法可以分析出,第二排的元素应该是从左到右递减。
当两排元素都确定了的时候,开始选择最佳路线,其实就是选择一列i,在i处向下。
假设第一排元素为
x
1
,
x
2
,
.
.
x
n
,
x_1,x_2,..x_n,
x1,x2,..xn,第二排元素是
y
1
,
y
2
.
.
,
y
n
y_1, y_2..,y_n
y1,y2..,yn
那么选i+1列转折比选i多出来的权值
d
w
=
x
i
+
1
−
y
i
dw=x_{i+1}-y_i
dw=xi+1−yi。由前面的分析可知,x递增,y递减,所以这个权值
d
w
dw
dw是递增的,因此从左到右选择转折点,总权值的变化要么是先减后增,要么是递增。那么显然应该选1或n为转折点。
因为第一排第一个和第二排最后一个是必选,所以把最小的两个元素分配给它们。然后剩下2(n-1)个元素分成两份n-1个元素的集合,使得权值最大的那个集合权值最小。
这样就转换成了一个01背包问题:容量为sum/2,在2(n-1)个物品中选n-1个物品,能装的最大重量是多少。
d
p
(
i
,
j
,
k
)
dp(i,j,k)
dp(i,j,k)表示考虑了前i个物品,选了j个物品,能否选出权值总和为k。因为还要记录路径,所以顺便用选择的最后一个物品编号来代表可行性。
感觉这题思维很巧妙,需要想到两步:
- 第一排和第二排有序的
- 通过上一步推出,最优走法要么在起点转折要么在最后转折。
比较开心的是用这种记录路径方式最后只用了32M的空间
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 5e4 + 10;
char dp[26][maxn*25];
int a[maxn];
int main()
{
int n; cin>>n;
n *= 2;
int sum = 0, num = (n-2)/2;
for(int i = 0; i < n; ++i) scanf("%d", &a[i]), sum += a[i];
sort(a,a+n);
sum -= a[0] + a[1];
int fix = sum/2;
//cout<<"fix:"<<fix<<endl;
dp[0][0] = -1;
int up = 0;
for(int i = 2; i < n; ++i){
for(int k = min(i-2,num-1); k >= 0; --k){
for(int j = min(up, fix-a[i]); j >= 0; --j){
if(dp[k][j] && !dp[k+1][j+a[i]] ) {
dp[k+1][j+a[i]] = i;
}
}
}
up += a[i];
}
int ans;
for(int i = fix; i >= 0; --i){
if(dp[num][i]) {
ans = i; break;
}
}
int vis[55]; memset(vis, 0, sizeof vis);
int id = dp[num][ans];
while(id != -1){
vis[id] = 1;
num--; ans -= a[id];
id = dp[num][ans];
}
cout<<a[0];
for(int i = 2; i < n; ++i) if(vis[i]) cout<<" "<<a[i]; cout<<endl;
for(int i = n-1; i >= 2; --i) if(!vis[i]) cout<<a[i]<<" "; cout<<a[1]<<endl;
return 0;
}