7.12 海高集训 DP 专题

#A. [USACO20OPEN] Exercise G

可以发现如果循环了几次可以回到原地,当且仅当形成了一个环,假如设每个环的长度是 \(a_i\),那么可以发现 \(k=\text{lcm}~a_i\),又知道 \(\text{lcm}\) 其实就是将那几个数分解质因数,然后取每个质数的最高次幂乘起来,所以我们可以想到枚举素数来做dp。

我们设 \(f(i,j)\) 表示前 \(i\) 个素数总和为 \(j\) 的所有 \(k\) 的总和,枚举第 \(i\) 个素数的幂进行转移,因为之前并没有用过第 \(i\) 个素数,所以应把上一个状态乘上 \(p_i^k\),所以直接有方程 \(f(i,j)=∑f(i−1,j−p_i^k)×p_i^k\)

接着发现这个东西可以滚动数组压缩一下,于是可以省掉一维 \(f(j)=∑f(j−p_i^k)×p_i^k\),倒序枚举即可,初始状态 \(f(0)=1\),最后答案是 \(∑f(i)\)

#include <bits/stdc++.h>
#define ll long long//注意开long long
using namespace std;
const int N = 1e4 + 3;
bool vis[N]; vector<int> p;
ll f[N] = {1}, m;int n;
int main (){
    p.push_back(0);//让素数的下标从一开始,使后面的转移更简洁
    cin >> n >> m;
    for (int i = 2; i <= n; i++){
        if (!vis[i])p.push_back(i);
        for (int j = i * i; j <= n; j += i)vis[j] = 1;
    }//埃筛筛素数
    for (int i = 1; i < p.size(); i++)
        for (int j = n; j >= p[i]; j--){
            int tmp = p[i];
            while(tmp <= j)
                f[j] = (f[j] + f[j - tmp] * tmp % m) % m, tmp *= p[i];
        }
    ll Ans = 0;
    for (int i = 0; i <= n; i++)Ans = (Ans + f[i]) % m;
    cout << Ans << endl;
    return 0;
}

[#B. [USACO20FEB] Delegation G]([题目详情 - USACO20FEB] Delegation G - 海亮教育信息学奥赛在线训练平台 (hailiangedu.com))

原图是一棵无根树,为了方便起见,我们指定 \(1\) 号点为根。

接下来我们从 \(1\) 号点开始遍历整棵树。

画个图后会发现,一条经过点 \(u\) 的链只可能是如下两种情况之一:

  • \(v_1→u→v_2\),其中 \(v_1,v_2\)\(u\) 子树内的某个点(特殊情况下,当然可以和 \(u\) 重合);
  • \(v→u→f\),其中 \(v\)\(u\) 子树内的某个点,\(f\)\(u\) 的某个祖先节点。

显然,对于某个点 \(u\),第二种链最多只有一条。

于是我们可以对每个点 \(u\),维护其等待配对的子链列表。每次遍历到 \(u\) 的一个子节点 \(v\) 时,将经过 \(v\) 的子链尝试与列表中已有的子链配对。如果配对失败就加入等待配对列表。

最后如果存在一个 \(u\) 满足其等待配对的子链大于等于两条(根据上面的推导,这种情况下肯定存在一条无法配对的孤链),则无解。

这个做法的效率如何呢?对于一个 \(K\),进行检验的时间显然是 \(O(N)\) 的,我们需要对 \(N−1\) 的每个因子都检验一遍,从而时间复杂度是 \(O(Nσ0(N−1))\) 的。

数据范围内最坏情况下,\(N=83161\)\(N−1\) 的因子有 \(128\) 个,在星形图(最多只有一个点的度数大于二)的情况下有被卡常数的风险。

这种情况下可以考虑对星形图特判处理。

#include <bits/stdc++.h>
using namespace std;
vector<int> Edge[100010];
int siz[100010], n;
inline bool dfs(int u, int Fa, int k)
{
	map<int, int> Map;
	for(auto v : Edge[u])
		if(v != Fa)
		{
			if(!dfs(v, u, k))
				return false;
			int tmp = k - siz[v];
			if(Map.count(tmp) && Map[tmp])
			{
				--Map[tmp];
				if(!Map[tmp])
					Map.erase(tmp);
				siz[u] -= tmp;
			}
			else if(k != siz[v])
			{
				siz[u] += siz[v];
				++Map[siz[v]];
			}
		}
		++siz[u];
		int now = 0;
		for(auto p : Map)
			now += p.second;
		return now <= 1;
}
int main()
{
	scanf("%d", &n);
	--n;
	for(int i = 1, u, v; i <= n; ++i)
	{
		scanf("%d %d", &u, &v);
		Edge[u].push_back(v);
		Edge[v].push_back(u);
	}
	for(int i = 1; i <= n; ++i)
		if(n % i)
			printf("0");
		else
		{
			memset(siz, 0, sizeof(siz));
			if(dfs(1, 0, i))
				printf("1");
			else printf("0");
		}
	return 0;
}

[#C. Tiles for Bathroom](题目详情 - Tiles for Bathroom - 海亮教育信息学奥赛在线训练平台 (hailiangedu.com))

给定一个 \(n\) 的正方形和正整数 \(q\),求对于所有正整数 \(k \le n\),有多少个 \(k \times k\) 的子正方形中所含
元素种数不超过 \(q\)

考虑从以每个点为右下角进行枚举,算出此时满足条件的最长边长。我们把合法条件转化一下可以得出:

只有在正方形中,除了当前点的颜色,只有 \(q−1\) 种不同的颜色可以存在。那么最短的非法边长就是除当前点颜色外第 \(q\) 种颜色被算入的最小边长。其减一就是最长合法边长。

现在的问题变为,如何判断一种颜色在边长增加到什么时候时会被计算。设右下角的点 \(a\) 坐标为 \((x1,y1)\),要求的点 \(b\) 坐标为 \((x2,y2)\),那么当正方形边长为 \(∣x1−x2∣+∣y1−y2∣\) 时该点会首次被计算入颜色种类中,因为要把横纵坐标都囊括到其中。我们发现这就是切比雪夫距离。

因此,我们维护出每个点切比雪夫距离前 \(k\) 小的颜色,这个可以通过左,上,左上三个方向进行转移。暴力即可。复杂度为 \(O(n^2qlogq)\)。实际上并没有这么大。

#include <bits/stdc++.h>
using namespace std;
int n, m, k, c, ans[1510], vis[2250050], dx[3] = {-1, 0, -1}, dy[3] = {0, -1, -1};
struct Node
{
	int d, c;
};
vector<Node> t, v[1510][1510];
inline bool cmp(Node x, Node y)
{
	return x.d < y.d;
}
inline void get(int x, int y)
{
	t.clear();
	t.push_back({1, c});
	for(int i = 0; i < 3; ++i)
		for(Node p : v[x + dx[i]][y + dy[i]])
			t.push_back({p.d + 1, p.c});
	sort(t.begin(), t.end(), cmp);
	for(Node p : t)
		if(!vis[p.c] && (int)v[x][y].size() <= m)
		{
			v[x][y].push_back(p);
			vis[p.c] = 1;
		}
	for(Node p : t)
		vis[p.c] = 0;
}
int main()
{
	scanf("%d %d", &n, &m);
	for(int i = 1; i <= n; ++i)
		for(int j = 1; j <= n; ++j)
		{
			scanf("%d", &c);
			get(i, j);
			k = min(i, j);
			if((int)v[i][j].size() > m)
				k = min(k, v[i][j][m].d - 1);
			++ans[k];
		}
	for(int i = n; i >= 1; --i)
		ans[i] += ans[i + 1];
	for(int i = 1; i <= n; ++i)
		printf("%d\n", ans[i]);
	return 0;
}

[#D. Karen and Supermarket](题目详情 - Karen and Supermarket - 海亮教育信息学奥赛在线训练平台 (hailiangedu.com))

在回家的路上,凯伦决定到超市停下来买一些杂货。 她需要买很多东西,但因为她是学生,所以她
的预算仍然很有限。
事实上,她只花了b美元。
超市出售N种商品。第i件商品可以以 \(c_i\) 美元的价格购买。当然,每件商品只能买一次。
最近,超市一直在努力促销。凯伦作为一个忠实的客户,收到了n张优惠券。
如果 Karen 购买第 i 件商品,她可以使用第 i 张优惠券,将该商品的价格减少 \(d_i\) 美元。 当然,不
买对应的商品,优惠券不能使用。
然而,对于优惠券有一个规则。对于所有i>=2,为了使用i张优惠券,凯伦必须也使用第 张优惠
券 (这可能意味着使用更多优惠券来满足需求。)
凯伦想知道。她能在不超过预算b的情况下购买的最大商品数量是多少?

很明显是在 \(01\) 背包加维操作, \(DP\) 的思路很好想,假设一个数字,存储在选第几个物品时,已经有几个被选了,且考虑用不用优惠券,即:

\(dp[i][sum][0/1]\)

但是数据量略大,不可以直接操作,要对其进行优化。考虑其有 \(n\) 件物品, \(n−1\) 个优惠券使用条件,于是考虑树形 \(DP\)

针对于一个新发现的子树我可以选择部分节点买入,对于原有子树也是同理,所以构建一个以 \(1\) 为根节点的子树,同时也要记录子树大小。

而该点要是要使用优惠券,则字数内至少有一个子树有用优惠劵。

结合这个思路得出状态转移方程为:

\(dp[u][i + j][0] = \min(dp[u][i + j][0], dp[u][i][0] + dp[v][j][0])\)

\(dp[u][i + j][1] = \min(dp[u][i + j][1], dp[u][i][1] + dp[v][j][1])\)

\(dp[u][i + j][1] = \min(dp[u][i + j][1], dp[u][i][1] + dp[v][j][0])\)

#include<bits/stdc++.h>
using namespace std;
long long n, B, siz[5050], dp[5050][5050][2], w[5050], d[5050];
vector<long long> Edge[5050];
inline void dfs(long long x)
{
	dp[x][0][0] = 0;
	dp[x][1][1] = w[x] - d[x];
	dp[x][1][0] = w[x];
	siz[x] = 1;
	for (long long y : Edge[x])
	{
		dfs(y);
		for (long long i = siz[x]; i >= 0; i--)
			for (long long j = 0; j <= siz[y]; j++)
			{
				dp[x][i + j][1] = min(dp[x][i + j][1], dp[x][i][1] + min(dp[y][j][1], dp[y][j][0]));
				dp[x][i + j][0] = min(dp[x][i + j][0], dp[x][i][0] + dp[y][j][0]);
			}
		siz[x] += siz[y];
	}
}
int main()
{
	memset(dp, 63, sizeof(dp));
	scanf("%lld %lld", &n, &B);
	for (long long i = 1, x; i <= n; i++)
	{
		scanf("%lld %lld", &w[i], &d[i]);
		if (i > 1)
		{
			scanf("%lld", &x);
			Edge[x].push_back(i);
		}
	}
	dfs(1);
	for (long long i = 1; i <= n + 1; i++)
		if (min(dp[1][i][0], dp[1][i][1]) > B)
			return !printf("%lld", i - 1);
	return 0;
}

[#E. Tree Elimination](题目详情 - Tree Elimination - 海亮教育信息学奥赛在线训练平台 (hailiangedu.com))

给定一棵 \(n\) 个节点的树,点编号 \(1 \sim n\),第 \(i\) 条边连接 \(a_i\)\(b_i\)
初始时你有一个空的序列,树上的 \(n\) 个点都有标记。
现在按照边的编号从小到大考虑每条边:

  1. 如果这条边连接的两个点都有标记,则选择其中一个点,擦除它的标记并将它的编号放入序列
    的末尾
  2. 否则什么都不做
    求能够由上述操作得到的不同的序列的数量。

首先,不同的序列数量等同于消除标记的方案数

考虑树形 dp。我们称一个点被一条边覆盖,表示这个点在考虑到这条边时选择消除了这个点上的标记。

\(f_{x,0/1/2/3}\) 分别表示:点 \(x\) 是被自己的父亲边之前的边覆盖的、点 \(x\) 是被自己的父亲边覆盖的、点 \(x\) 是被自己的父亲边之后的边覆盖的、点 \(x\) 没有被覆盖。

那么对于点 \(y∈son_x\),考虑 \(f_{x,0/2}\)

  • \(y\) 不能被覆盖,即 \(f_{y,2/3}\)
  • 定义点 \(z(∈son_x)<y\) 是指边 \((x,z)\) 在边 \((x,y)\) 之前,则点 \(z\) 一定要被覆盖,即 \(f_{z,0/1}\)
  • 定义点 \(z(∈son_x)>y\) 是指边 \((x,z)\) 在边 \((x,y)\) 之后,则点 \(z\) 可以被覆盖也可以不被覆盖,但不能被边 \((x,z)\) 覆盖,即 \(f_{z,0/2/3}\)

因此有转移:

\(f_{x,0/2}=∑_{y∈son_x}(f_{y,2/3}∗∏_{z<y}f_{z,0/1}∗∏_{z>y}f_{z,0/2/3})\)

考虑 \(f_{x,1}\),有转移:

\(f_{x,1}=∏_{y<fa_x}f_{y,0/1}∗∏_{y>fa_x}f_{y,0/2/3}\)

考虑 \(f_{x,3}\),有转移:

\(f_{x,3}=∏_yf_{y,0/1}\)

时间复杂度 \(O(n)\)

#include <bits/stdc++.h>
#define mod 998244353
using namespace std;
long long n, p1, p2, dp[200020][4], a[200020], b[200020], c[200020];
vector<long long> G[200020];
inline void dfs(long long x, long long fa)
{
	for (auto y : G[x])
		if (y != fa)
			dfs(y, x);
	p1 = p2 = 0;
	a[0] = 1;
	for (auto y : G[x])
		if (y == fa)
			p2 = p1;
		else
		{
			++p1;
			a[p1] = (a[p1 - 1] * (dp[y][0] + dp[y][1])) % mod;
			b[p1] = (dp[y][2] + dp[y][3]) % mod;
			c[p1] = (dp[y][0] + b[p1]) % mod;
		}
	c[p1 + 1] = 1;
	for (long long i = p1; i >= 1; i--)
		c[i] = c[i] * c[i + 1] % mod;
	for (long long i = 1; i <= p1; i++)
		dp[x][(i > p2) * 2] = (dp[x][(i > p2) * 2] + a[i - 1] * b[i] % mod * c[i + 1] % mod) % mod;
	dp[x][1] = a[p2] * c[p2 + 1] % mod;
	dp[x][3] = a[p1];
}
int main()
{
	scanf("%lld", &n);
	for (long long i = 1, x, y; i < n; i++)
	{
		scanf("%lld %lld", &x, &y);
		G[x].push_back(y);
		G[y].push_back(x);
	}
	dfs(1, 0);
	printf("%lld", (dp[1][0] + dp[1][2] + dp[1][3]) % mod);
	return 0;
}

[#F. Miss Punyverse](题目详情 - Miss Punyverse - 海亮教育信息学奥赛在线训练平台 (hailiangedu.com))

给定一棵 \(n\) 个节点的树,每个点有点权 \(b_i\)\(w_i\),请将这棵树划分为 \(m\) 个连通块,使得满足以下条件
的连通块个数最大化:连通块内点的 \(w\) 之和严格大于连通块内点的 \(b\) 之和

根据数据范围,发现是个 \(O(n^2)\) 的题。再一看,直接贪心似乎不行,因为你考虑假设现在断开了一个联通块,并且它是好的,但是假设把这个联通快再多加入一些节点,它还是好的,那么它可能让答案更优,因为当前已经选的联通快变少了,那么剩下可以分的联通块数也变多了,所以其它节点可能能创造更多好的联通快。那么不是贪心,也找不到什么特别好的性质,并且是一类最优解问题,不难想到动规。

我们再一思考,这道题是在树上,树上 \(n^2\) 的动规有哪几种可能?

  1. 其一,最简单的,一维状态,由子节点或者父节点转移过去。但是显然你想不太到这么简单的方案;
  2. 二维的状态,那第二维该弄个什么好呢?发现好像只能是块的个数。但是发现这样就变成树上背包了,也就是下面那种。
  3. 那就是树上背包。

我们来具体讲一下怎么树上背包。首先状态不说,肯定是 \(f[u][i]\) 表示以 \(u\) 这个节点为根的子树分成 \(i\) 块,不算 \(u\) 所处的那一块的最大值,然后我们称一个点的权值为 \(a_i\) ,其等于 \(w_i−b_i\) 。然后我们经思考后,发现好像不能直接去树上背包,原因是你还是不知道当前根节点 \(u\) 这一块的权值是多少。

当时我就愣住了,之后便认为这道题恐怖如斯,不是我能做出来的题。但这时候就应该去找一些性质或者贪心策略以使这个问题可以迎刃而解。所以我们找到了一个非常漂亮的性质(贪心策略):我们思考两种方案,第一种的块数比第二种多,但是第二种 \(u\) 那个联通块的权值和大于第一种。

为了防止搞混两种,再清晰地理一下:

  1. 块数多
  2. \(u\) 那个联通块的权值大

第一种一定 不劣于 第二种!

我们采取反证法,思考假设第二种比第一种优会优再什么情况下:可能存在第二种他的权值很大,然后把上边权值是非正数的一块变成正数了,然后会使答案加 11 ,那么我们思考第一种,他可以把权值是非正数的那块不去管他,那么他在最差情况下也就是第二种在最好情况下会和第二种一样。那么什么情况下第一种会比第二种优呢?就比如说 \(u\) 上面的所有节点权值都是正数,那么第二种就会比第一种要差。

那么假设有两种方案块数相等,不难想到u那块必定是权值越大越好,因为可能越大。那么我们再用一个 \(g[u][i]\) 表示在块数为 \(f[u][i]\)\(u\) 那个联通块的最大权值。

最后给转移方程(这里用的是刷表法):

  1. \(v\) 所处的联通块不和 \(u\) 合并,即单独划分

    \(f_{u,i}+f_{v,j}+(g_{v,j}>0),g_{u,i}\) 去更新 \(f_{u,i+j},g_{u,i+j}\) 的最优值。

  2. \(v\) 的那个联通块与 \(u\) 合并

    \(f_{u,i}+f_{v,j},g_{u,i}+g_{v,j}\) 去更新 \(f_{u,i+j−1},g_{u,i+j−1}\) 的最优值。

边界是 \(f[u][1]=0,g[u][1]=a_u\)

#include<bits/stdc++.h>
using namespace std;
vector<int> G[3030];
int T, n, m, b[3030], w[3030], siz[3030];
pair<int, long long> f[3030][3030], t[3030];
inline void dfs(int u, int fa)
{
	siz[u] = 1;
	f[u][1] = make_pair(0, w[u]);
	for (int v : G[u])
	{
		if (v == fa)
			continue;
		dfs(v, u);
		for (int i = 1; i <= siz[u]; ++i)
			for (int j = 1; j <= siz[v]; ++j)
			{
				t[i + j - 1] = max(t[i + j - 1], make_pair(f[u][i].first + f[v][j].first, f[u][i].second + f[v][j].second));
				t[i + j] = max(t[i + j], make_pair(f[u][i].first + f[v][j].first + (f[v][j].second > 0), f[u][i].second));
			}
		siz[u] += siz[v];
		for (int i = 1; i <= siz[u]; ++i)
		{
			f[u][i] = t[i];
			t[i] = make_pair(0, -1e18);
		}
	}
}
int main()
{
	scanf("%d", &T);
	while (T--)
	{
		scanf("%d %d", &n, &m);
		for (int i = 1; i <= n; ++i)
		{
			scanf("%d", &b[i]);
			G[i].clear();
		}
		for (int i = 1; i <= n; ++i)
		{
			scanf("%d", &w[i]);
			w[i] -= b[i];
		}
		for (int i = 1, u, v; i < n; ++i)
		{
			scanf("%d %d", &u, &v);
			G[u].push_back(v);
			G[v].push_back(u);
		}
		for (int i = 1; i <= n; ++i)
			t[i] = make_pair(0, -1e18);
		dfs(1, 0);
		printf("%d\n", f[1][m].first + (f[1][m].second > 0));
	}
	return 0;
}

[#G. [NOIP2017 提高组] 宝藏]([题目详情 - NOIP2017 提高组] 宝藏 - 海亮教育信息学奥赛在线训练平台 (hailiangedu.com))

状态:

\(f_{i,j,S}(j \in S)\),表示从起点到节点 \(j\) 的距离为 \(i\),我现在要从 \(j\) 开始挖边,挖通集合 \(S\) 的最小代价。

转移时,我枚举从节点 \(j\) 打出的第一条边,假设它是 \(j->k\),再枚举从k要打到的子集\(S_2⊂S\),那么

\(f_{i,j,S}=min_{k \in S_2⊂S}(d[j][k]∗(i+1)+f_{i+1,k,S_2−\{k\}}+f_{i,j,S−S_2})\)

最后取所有 \(f_{0,i,U-\{i\}}\) 的最小值即可,其中 \(U\) 是全集。

#include <iostream>
#include <cstdio>
using namespace std;
long long n, m, Edge[15][15], dp[15][1 << 15];
signed main()
{
    scanf("%lld %lld", &n, &m);
    for (long long i = 1; i <= n; i++)
        for (long long j = 1; j <= n; j++)
            Edge[i][j] = 1e18;
    for (long long i = 1, x, y, z; i <= m; i++)
	{
        scanf("%lld %lld %lld", &x, &y, &z);
        Edge[x][y] = min(Edge[x][y], z);
		Edge[y][x] = min(Edge[y][x], z);
    }
    for (long long i = 0; i <= n; i++)
        for (long long S = 0; S < (1 << n); S++)
            dp[i][S] = 1e18;
    for (long long i = 1; i <= n; i++)
        dp[0][1 << i - 1] = 0;
    for (long long i = 0; i < n; i++)
        for (long long S = 0; S < (1 << n); S++)
		{
            if (dp[i][S] == 1e18)
                continue;
            long long x = (1 << n) - 1 - S;
            for (long long T = x; T; T = (T - 1) & x)
			{
                long long sum = 0, flag = 1;
                for (long long j = 1; j <= n; j++)
                    if (T & (1 << j - 1))
					{
                        long long minn = 1e18;
                        for (long long k = 1; k <= n; k++)
                            if (S & (1 << k - 1))
                                minn = min(minn, Edge[j][k]);
                        if (minn == 1e18)
                            flag = 0;
                        sum += minn;
                    }
                if (flag)
                    dp[i + 1][S + T] = min(dp[i + 1][S + T], dp[i][S] + sum * (i + 1));
            }
        }
    long long ans = 1e18;
    for (long long i = 0; i <= n; i++)
        ans = min(ans, dp[i][(1 << n) - 1]);
    printf("%lld", ans);
    return 0;
}

[#H. 修缮长城](题目详情 - 修缮长城 - 海亮教育信息学奥赛在线训练平台 (hailiangedu.com))

因为只要经过就会修所以最优的修缮策略是从起点开始往左或往右修一段区间,所以考虑反向区间dpdpdp。

\(dp\_{i, j}\) 表示修缮完区间 \([i, j]\) 后的时间内再修其它区间产生的花费。

那么转移时不需要考虑区间 \([i, j]\) ,只需要考虑除了区间 \([i, j]\) 以外的点。

发现修缮区间 \([i, j]\) 时花费的时间会对以后要修复的点产生影响,具体的,还没有修复的那些点对答案的贡献会加上从区间 \([i, j]\) 转移到下个区间的时间乘单位时间的代价。

所以只需计算区间和区间转移时花费的时间。

注意到修缮完一个区间后一定是在区间的左/右端点,所以可以设 \(dp_{i, j, 0/1}\) 表示当前修缮到区间位 \([i, j]\) 于左/右端点,以后的时间内修缮完其他的点的代价。

\(s_i\) 表示区间 \([1, i]\) 内的 $ \Delta$ 的和,\(len_{i, j}\)表示点 \(i\) 到点 \(j\) 的距离。很容易的列出 \(dp\) 式子:

\(cost = s_n - (s_j - s_{i - 1})\)

\(dp_{i, j, 0} \leftarrow dp_{i - 1, j, 0} + \frac{cost \times len_{i - 1, i}}{v}\)

\(dp_{i, j, 0} \leftarrow dp_{i, j + 1, 1} + \frac{cost \times len_{i, j + 1}}{v}\)

$dp_{i, j, 1} \leftarrow dp_{i - 1, j, 0} + \frac{cost \times len_{i - 1, j}}{v} $

\(dp_{i, j, 1} \leftarrow dp_{i, j + 1, 1} + \frac{cost \times len_{j, j + 1}}{v}\)

#include <bits/stdc++.h>
using namespace std;
int n, x;
double dp[1010][1010][2], sum[1010], ans, v;
struct Point
{
	int pos;
	double Val, delta;
}point[1010];
inline bool Cmp(Point a, Point b)
{
	return a.pos < b.pos;
}
int main()
{
	while (scanf("%d %lf %d", &n, &v, &x) == 3)
	{
		if (!n && !v && !x)
			return 0;
		for (int i = 1; i <= n; i++) 
			scanf("%d %lf %lf", &point[i].pos, &point[i].Val, &point[i].delta);
		point[++n].pos = x;
		point[n].Val = point[n].delta = 0.0;
		sort(point + 1, point + n + 1, Cmp);
		int pos;
		for (int i = 1; i <= n; i++) 
			if (point[i].pos == x && point[i].delta == 0.0 && point[i].Val == 0.0)
			{
				pos = i;
				break;
			}
		ans = 0;
		for (int i = 1; i <= n; i++) 
		{
			sum[i] = sum[i - 1] + point[i].delta;
			ans += point[i].Val;
		}
		for (int i = 1; i <= n; i++) 
			for (int j = 1; j <= n; j++)
				dp[i][j][0] = dp[i][j][1] = 1e9;
		dp[1][n][0] = dp[1][n][1] = 0.0; 
		for (int l = n - 1; l >= 1; l--)	
			for (int i = 1; i + l - 1 <= n; i++)
			{
				int j = i + l - 1;
				double cost = sum[n] - sum[j] + sum[i - 1];
				if (i - 1 >= 1)
					dp[i][j][0] = min(dp[i][j][0], dp[i - 1][j][0] + cost * (double)(point[i].pos - point[i - 1].pos) / v);
				if (j + 1 <= n)
					dp[i][j][0] = min(dp[i][j][0], dp[i][j + 1][1] + cost * (double)(point[j + 1].pos - point[i].pos) / v);
				if (j + 1 <= n)
					dp[i][j][1] = min(dp[i][j][1], dp[i][j + 1][1] + cost * (double)(point[j + 1].pos - point[j].pos) / v);
				if (i - 1 >= 1)
					dp[i][j][1] = min(dp[i][j][1], dp[i - 1][j][0] + cost * (double)(point[j].pos - point[i - 1].pos) / v);
			}
		printf("%d\n", (int)floor(ans + min(dp[pos][pos][0], dp[pos][pos][1])));
	}
	return 0;
}

\(\text{I 和 J 都是 NOI 系列题目,应该大家都会,于是就不写了(什}\)

posted @ 2023-07-12 14:04  Naitoah  阅读(49)  评论(0)    收藏  举报