题解:P14508 猜数游戏 guess
Blog
赛时大脑获得了犯困 debuff,成功宕机,11:30 才想出了这题。
看上去比较像交互最优化题,可以考虑一下对策略进行 DP。注意到一段区间的答案只与其长度有关,而与其具体对应的下标区间无关,因此定义
因为我们的一般策略是没遇到目标的时候一直走,遇到目标了折返回来,所以实际上转移的关键就在于如何决定每一次折返后该选择走什么长度。先考虑一个暴力转移,考虑枚举折返时选择的长度
- 走完后的代价:
\max\{dp_l, dp_{i - l}\} 。 - 从端点开始位移
j 的代价dis_j 。
因此转移方程为
发现我们只需要求解
- 对于 DP 过程中的查询:只会查询
0\le j \le V 的部分。 - 对于最短路过程中的转移:容易发现我们可以任意交换操作的顺序,而不影响最终的终点。因此在当前位置
> V 时,我们优先执行向小的数走,在当前位置\le V 的时候,我们优先执行向大的数走。因为每条边最多跨过的点就是O(a_i) 级别的,所以这样交换操作的顺序,我们一定不会走出2000 这个上界。直接对这2000 个点跑最短路即可。
注意到这张图是个稠密图,因此跑堆优化 Dijkstra 是不优的(堆优化的复杂度与边数相关),比朴素 Dijkstra 多一个
时间复杂度
#include <bits/stdc++.h>
#define fi first
#define se second
using namespace std;
typedef long long ll;
using pi = pair<int, int>;
using pl = pair<int, ll>;
const int N = 10005, M = 1005, V = 2005;
const ll inf = 0x3f3f3f3f3f3f3f3f;
ll n, m, a[N], b[N];
ll dis[V], dp[N];
bool legal(ll x)
{
return (0 <= x && x < V);
}
void solve()
{
cin >> n >> m;
ll amx = 0;
for(int i = 1; i <= m; i++)
{
cin >> a[i];
amx = max(amx, a[i]);
}
for(int i = 1; i <= m; i++) cin >> b[i];
memset(dis, 0x3f, sizeof(dis));
dis[0] = 0;
bitset<V> vis;
for(int r = 0; r < V; r++)
{
int u = -1;
for(int i = 0; i < V; i++)
{
if(vis[i]) continue;
if(u == -1 || dis[u] > dis[i]) u = i;
}
if(u == -1 || dis[u] >= inf) break;
vis[u] = 1;
for(int i = 1; i <= m; i++)
{
int v = u + a[i];
if(legal(v) && dis[v] > dis[u] + b[i])
dis[v] = dis[u] + b[i];
v = u - a[i];
if(legal(v) && dis[v] > dis[u] + b[i])
dis[v] = dis[u] + b[i];
}
}
memset(dp, 0x3f, sizeof(dp));
dp[1] = 0;
for(int i = 2; i <= n; i++)
{
for(int j = 1; j <= min(i - 1ll, amx); j++)
{
ll res = max(dp[j], dp[i - j]) + dis[j];
dp[i] = min(dp[i], res);
}
}
if(dp[n] >= inf) cout << "-1\n";
else cout << dp[n] << "\n";
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int sid, t;
cin >> sid >> t;
while(t--) solve();
return 0;
}