2023 5月 dp做题记录
- 5月 dp做题记录
- P1064 [NOIP2006 提高组] 金明的预算方案
- P1941 [NOIP2014 提高组] 飞扬的小鸟
- P2679 [NOIP2015 提高组] 子串
- P1850 [NOIP2016 提高组] 换教室
- P2831 [NOIP2016 提高组] 愤怒的小鸟
- P5020 [NOIP2018 提高组] 货币系统
- P6064 [USACO05JAN]Naptime G
- P9344 去年天气旧亭台
- P4095 [HEOI2013]Eden 的新背包问题
- P3174 [HAOI2009] 毛毛虫
- P2340 [USACO03FALL]Cow Exhibition G
- P4059 [Code+#1]找爸爸
- P4342 [IOI1998]Polygon
- CF149D Coloring Brackets
- UVA12991 Game Rooms
- Generate a String
- Games with Rectangle
- CF837D Round Subset
- CF14D Two Paths
- CF527D Clique Problem
- P4310 绝世好题
- P4158 [SCOI2009]粉刷匠
- P1772 [ZJOI2006] 物流运输
- P3861 拆分
5月 dp做题记录
P1064 [NOIP2006 提高组] 金明的预算方案 #
物体与物体之间有从属的限制关系,且从属关系只有一层,最多有两个个附件,所以我们很容易就可以想到列举出每一组的组合关系,毕竟每组最多四种组合,再跑一遍分组背包,从每组中至多选出一种组合,以满足限制条件。
P1941 [NOIP2014 提高组] 飞扬的小鸟 #
发现题目是一张
再注意细节,如果跳跃过高是会顶格的,所以最高行是可以通过前一列所有小于跳跃高度的点转移的,这点要单独枚举。还有背包枚举顺序的问题,如果下落是不会再上升的,所以是先上升的 01 背包,再上升的完全背包,最后在下落的 01 背包,防止下落的状态被完全背包转移。
这题要注意的就是不能少转移,因为用的是滚动数组,前面的都要继承下来。
P2679 [NOIP2015 提高组] 子串 #
匹配问题,有状态
初始状态
答案为
#include <bits/stdc++.h>
using namespace std;
int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c - '0');
c = getchar();
}
return x * f;
}
int n, m, g;
char a[1010], b[1010];
long long dp[2][210][210][2];
int main(){
n = read(), m = read(), g = read();
scanf("%s%s", a + 1, b + 1);
dp[0][0][0][0] = dp[1][0][0][0] = 1;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
for(int k = 1; k <= g; k++){
dp[i & 1][0][0][0] = 1, dp[i & 1 ^ 1][0][0][0] = 1, dp[i & 1][j][k][0] = 0, dp[i & 1][j][k][1] = 0;
dp[i & 1][j][k][0] = (dp[i & 1 ^ 1][j][k][0] + dp[i & 1 ^ 1][j][k][1]) % 1000000007;
if(a[i] == b[j]){
dp[i & 1][j][k][1] = (dp[i & 1 ^ 1][j - 1][k][1] + dp[i & 1 ^ 1][j - 1][k - 1][1] + dp[i & 1 ^ 1][j - 1][k - 1][0]) % 1000000007;
}
}
}
}
cout << (dp[n & 1][m][g][0] + dp[n & 1][m][g][1]) % 1000000007 << endl;
return 0;
}
P1850 [NOIP2016 提高组] 换教室#
因为是选择性问题,所以状态就很套路,定义状态为
#include<bits/stdc++.h>
using namespace std;
int n, m, a, b;
int c[2010], d[2010];
double k[2010], ans, dp[2010][2010][2], f[310][310];
int main(){
cin >> n >> m >> a >> b;
for(int i = 1; i <= n; i++) cin >> c[i];
for(int i = 1; i <= n; i++) cin >> d[i];
for(int i = 1; i <= n; i++) cin >> k[i];
for(int i = 1; i <= a; i++){
for(int j = 1; j <= a; j++) f[i][j] = 2147483647;
}
for(int i = 1; i <= b; i++){
int u, v;
double w;
cin >> u >> v >> w;
f[u][v] = f[v][u] = min(f[u][v], w);
}
for(int k = 1; k <= a; k++){
f[k][k] = 0;
for(int i = 1; i <= a; i++){
for(int j = 1; j <= a; j++){
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}
}
}
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++) dp[i][j][0] = dp[i][j][1] = 2147483647;
}
dp[1][0][0] = dp[1][1][1] = 0;
for(int i = 2; i <= n; i++){
for(int j = 0; j <= min(i, m); j++){
dp[i][j][0] = min(dp[i - 1][j][0] + f[c[i - 1]][c[i]], dp[i - 1][j][1] + f[c[i - 1]][c[i]] * (1 - k[i - 1]) + f[d[i - 1]][c[i]] * k[i - 1]);
if(j != 0) dp[i][j][1] = min(dp[i - 1][j - 1][0] + f[c[i - 1]][c[i]] * (1 - k[i]) + f[c[i - 1]][d[i]] * k[i], dp[i - 1][j - 1][1] + f[c[i - 1]][c[i]] * (1 - k[i - 1]) * (1 - k[i]) + f[c[i - 1]][d[i]] * (1 - k[i - 1]) * k[i] + f[d[i - 1]][c[i]] * k[i - 1] * (1 - k[i]) + f[d[i - 1]][d[i]] * k[i - 1] * k[i]);
}
}
ans = 0x3f3f3f3f;
for(int j = 0; j <= m; j++){
ans = min(ans, dp[n][j][0]);
if(j != 0) ans = min(ans, dp[n][j][1]);
}
cout << fixed << setprecision(2) << ans << endl;
return 0;
}
P2831 [NOIP2016 提高组] 愤怒的小鸟#
这题需要一些数学知识,首先看数据范围,确定是状压 dp,简单的定义状态
横坐标不同的两点确定一条形式为
②式
①-③
同理得,
这样的复杂度是
#include<bits/stdc++.h>
using namespace std;
int t, n, m;
double eps = 1e-8, x[20], y[20];
int line[20][20], dp[1 << 20], lowbit[1 << 20];
void findline(double &a, double &b, double x1, double y1, double x2, double y2){
a = -(y1 * x2 - y2 * x1) / (x2 * x2 * x1 - x1 * x1 * x2);
b = (y1 * x2 * x2 - y2 * x1 * x1) / (x1 * x2 * x2 - x2 * x1 * x1);
}
int main(){
cin >> t;
for(int i = 0; i < (1 << 18); i++){
for(int j = 1; j <= 18; j++){
if((i & (1 << (j - 1))) == 0){
lowbit[i] = j;
break;
}
}
}
while(t--){
memset(line, 0, sizeof(line));
memset(dp, 1, sizeof(dp));
cin >> n >> m;
for(int i = 1; i <= n; i++){
scanf("%lf%lf", &x[i], &y[i]);
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
if(fabs(x[i] - x[j]) < eps) continue;
double a, b;
findline(a, b, x[i], y[i], x[j], y[j]);
if(a > -eps) continue;
for(int k = 1; k <= n; k++){
if(fabs(a * x[k] * x[k] + b * x[k] - y[k]) < eps) line[i][j] |= (1 << (k - 1));
}
}
}
dp[0] = 0;
for(int i = 0; i < (1 << n) - 1; i++){
int j = lowbit[i];
dp[i | (1 << (j - 1))] = min(dp[i | (1 << (j - 1))], dp[i] + 1);
for(int k = 1; k <= n; k++) dp[i | line[j][k]] = min(dp[i | line[j][k]], dp[i] + 1);
}
cout << dp[(1 << n) - 1] << endl;
}
return 0;
}
P5020 [NOIP2018 提高组] 货币系统#
要把
我们想验证的猜想其实就是证
要证
有了这个结论,就可以证
知道了
P6064 [USACO05JAN]Naptime G#
对于每一段,我们都有睡或不睡两种选择,并且是选段问题,并且当前的效用值有没有贡献取决于上一次有没有睡觉,所以我们的状态可以是
如果这题不可以睡到第二天早上,那么就这样就行了,但是我们发现这题是可以从第一天连着睡到第二天的前几段的。但是容易发现这其中的区别其实就是多了第一段的效用值,其他段并没有影响,因为每一段还是都只能睡一次,不会重复睡。这里有一个技巧,可以把睡觉分为两种情况,一种没有第一段,另一种强制选第一段。在初始化时,把
P9344 去年天气旧亭台 #
选若干段区间
每次都要枚举前面所有的
P4095 [HEOI2013]Eden 的新背包问题 #
对于每一次询问,给出不能选的一个物品
答案即
其中
P3174 [HAOI2009] 毛毛虫 #
要找到一条链,使得链上的点加上点延伸出去的点最多,我们可以发现,对于一个点
这题的状态还可以更简单一点,可以去掉
后面学完了树的直径,这题还可以更简单,其实就是我们现在每个点的最长链不是
P2340 [USACO03FALL]Cow Exhibition G #
这题虽然是黄题,但是让我注意到了之前
要求在满足智商情商非负的情况下,智商情商和最大。我们平时写
但我们想在这题里面同时知道智商和情商的花费情况,我们就需要初始化了,因为发现如果初始化了,状态描述就变成了
所以在这题里,我们随便用一个智商表示重量,求此时情商最大。防止数组越界,我们把重量加一个大的值。最后我们枚举正的智商
P4059 [Code+#1]找爸爸 #
序列匹配问题,一般状态
我们观察四个情况,很容易发现最后一个情况是对答案一定是负面影响,因为我们加空格,是为了接近匹配的目标,而第四种不仅接近不了目标,还减少相似度,所以可以直接去掉。
最后我们的状态
注意这题的初始化比较多,大多是不可能达到或者不优的,比如
这题细节还是多,要把细节好好回味一下
#include <bits/stdc++.h>
using namespace std;
int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c - '0');
c = getchar();
}
return x * f;
}
string A, B;
int x, y;
int a[3010], b[3010], d[6][6], dp[3010][3010][3];
int find(char a){
if(a == 'A') return 1;
if(a == 'T') return 2;
if(a == 'G') return 3;
return 4;
}
int main(){
cin >> A >> B;
int n = A.length(), m = B.length();
for(int i = 0; i < n; i++) a[i + 1] = find(A[i]);
for(int i = 0; i < m; i++) b[i + 1] = find(B[i]);
for(int i = 1; i <= 4; i++){
for(int j = 1; j <= 4; j++) cin >> d[i][j];
}
cin >> x >> y;
for(int i = 1; i <= max(n, m); i++){
dp[i][0][0] = dp[0][i][0] = dp[i][0][1] = dp[0][i][2] = -0x3f3f3f3f;
dp[i][0][2] = dp[0][i][1] = -x - y * (i - 1);
}
dp[0][0][1] = dp[0][0][2] = -0x3f3f3f3f;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
dp[i][j][0] = max(dp[i - 1][j - 1][0], max(dp[i - 1][j - 1][1], dp[i - 1][j - 1][2])) + d[a[i]][b[j]];
dp[i][j][1] = max(dp[i][j - 1][0] - x, max(dp[i][j - 1][1] - y, dp[i][j - 1][2] - x));
dp[i][j][2] = max(dp[i - 1][j][0] - x, max(dp[i - 1][j][1] - x, dp[i - 1][j][2] - y));
}
}
cout << max(dp[n][m][0], max(dp[n][m][1], dp[n][m][2])) << endl;
return 0;
}
P4342 [IOI1998]Polygon#
一眼环形
这道题就是普通的合并,所以设状态
但是我们发现,这样子每次区间都取最大最后合并出的不一定是最大的,原因就是数字中有负数,有可能我们的区间是负数乘负数得出的最大值。所以我们思考一下,发现大区间最大价值只跟小区间的最大值和最小值有关。所以我们可以维护两个状态
分类讨论,断点是加法,最大值和最小值互不影响;断点是乘法,排列组合一下,最大乘最大,最大乘最小,最小乘最大,最大乘最小,最大值最小值乘法都一样。
答案便为
CF149D Coloring Brackets#
一道经典的区间染色题,特别的是由于括号的原因,任意的一段区间不一定是合法的,这些本身就不合法的区间是不好用来转移的。
我们要让状态转移,关心的地方就是题目给出的性质,两两相邻的括号颜色不同,而区间上特别的就是两个端点,这是从小区间转移到大区间我们最需要考虑的两点,所以我们设状态
状态列出来,我们要考虑最开始的问题,怎样保证每次转移的状态都是配对的括号序列呢?递推是不好排除特殊情况的。很简单的想法就是我们自己考虑往哪转移,可以用记忆化搜索,从大的合法序列推向小的合法序列。
首先,每个左括号对应的右括号是可以预处理出来的,用栈模拟匹配括号,看到左括号就推进去;看到右括号,它对应的一定是栈顶的左括号。
我们考虑一个合法的括号序列
初始状态就是当
对于第一种情况,我们先递归
对于第二种情况,先递归求出
这题由于区间的是否合法性(毕竟只有先合法才能转移),可以用记忆化搜索,自己考虑往合法的区间递归。
/*
* @Author: Fire_Raku
* @Date: 2023-05-21 12:06:46
* @Last Modified by: Fire_Raku
* @Last Modified time: 2023-05-21 12:22:05
*/
#include <bits/stdc++.h>
using namespace std;
const int mod = 1000000007;
int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c - '0');
c = getchar();
}
return x * f;
}
char s[710];
long long c[710], dp[710][710][3][3], ans;
stack<int> st;
void dfs(int l, int r){
if(l + 1 == r){
dp[l][r][0][1] = dp[l][r][1][0] = dp[l][r][2][0] = dp[l][r][0][2] = 1;
return;
}
if(c[l] == r){
dfs(l + 1, r - 1);
for(int i = 0; i <= 2; i++){
for(int j = 0; j <= 2; j++){
if(j != 1) dp[l][r][0][1] += dp[l + 1][r - 1][i][j], dp[l][r][0][1] %= mod;
if(j != 2) dp[l][r][0][2] += dp[l + 1][r - 1][i][j], dp[l][r][0][2] %= mod;
if(i != 1) dp[l][r][1][0] += dp[l + 1][r - 1][i][j], dp[l][r][1][0] %= mod;
if(i != 2) dp[l][r][2][0] += dp[l + 1][r - 1][i][j], dp[l][r][2][0] %= mod;
}
}
return;
}
else{
dfs(l, c[l]), dfs(c[l] + 1, r);
for(int i = 0; i <= 2; i++){
for(int j = 0; j <= 2; j++){
for(int k = 0; k <= 2; k++){
for(int p = 0; p <= 2; p++){
if((k != p) || (!k && !p)) dp[l][r][i][j] += (dp[l][c[l]][i][k] * dp[c[l] + 1][r][p][j]) % mod;
}
}
}
}
return;
}
}
int main(){
cin >> s + 1;
int n = strlen(s + 1);
for(int i = 1; i <= n; i++){
if(s[i] == ')'){
c[st.top()] = i;
c[i] = st.top();
st.pop();
}
else st.push(i);
}
dfs(1, n);
for(int i = 0; i <= 2; i++){
for(int j = 0; j <= 2; j++) ans += dp[1][n][i][j], ans %= mod;
}
cout << ans << endl;
return 0;
}
UVA12991 Game Rooms#
求如何放乒乓球桌和游泳池,才能让楼层的人移动总和最小。状态里肯定有当前位置放的是什么来转移,并且因为放的数量没有限制,我们肯定的全放满最优。然后我们观察他们移动的距离,如果放的是乒乓球桌,那么喜欢游泳池的人们,就要走到离他们最近的游泳池,换句话说,就是走出他们所在的极大乒乓球区间,放游泳池同理。所以我们有一个结论,可以把楼层分为一个个连续的兵乓球桌区间和游泳池区间,并且一个区间固定,他里面的人们移动的总距离也就确定。
我们可以预处理出人们移动的距离,
它一阶一阶的,很像我们要的距离。设区间
显然,如果区间
有了这个,我们还需要状态。我们需要知道同色区间,但是现在两端都不知道在哪。这题里我们要转移,就必须要知道某一段全是同一种的区间。而如果我们用区间 dp,很难去表示它(因为
#include<bits/stdc++.h>
using namespace std;
int T, n, cas;
long long pre[4010][2], suf[4010][2], prep[4010][2], suff[4010][2], t[4010], p[4010], dp[4010][2];
void init(){
memset(pre, 0, sizeof(pre));
memset(suf, 0, sizeof(suf));
memset(prep, 0, sizeof(prep));
memset(suff, 0, sizeof(suff));
}
long long cost(int l, int r, int co){
if(l == 1 && r == n) return 0x3f3f3f3f3f3f3f;
if(l == 1) return prep[r][co];
if(r == n) return suff[l][co];
int mid = (l + r) >> 1;
long long tot = 0;
tot += suff[l][co] - suff[mid + 1][co] - (mid - l + 1) * suf[mid + 1][co];
tot += prep[r][co] - prep[mid][co] - (r - mid) * pre[mid][co];
return tot;
}
int main(){
cin >> T;
for(int k = 1; k <= T; k++){
init();
cas++;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> t[i] >> p[i];
pre[i][0] = pre[i - 1][0] + t[i];
pre[i][1] = pre[i - 1][1] + p[i];
prep[i][0] = prep[i - 1][0] + pre[i][0];
prep[i][1] = prep[i - 1][1] + pre[i][1];
}
for(int i = n; i >= 1; i--){
suf[i][0] = suf[i + 1][0] + t[i];
suf[i][1] = suf[i + 1][1] + p[i];
suff[i][0] = suff[i + 1][0] + suf[i][0];
suff[i][1] = suff[i + 1][1] + suf[i][1];
}
for(int i = 1; i <= n; i++) dp[i][0] = dp[i][1] = 0x3f3f3f3f3f3f3f;
dp[0][0] = dp[0][1] = 0;
for(int i = 1; i <= n; i++){
for(int j = 0; j < i; j++){
dp[i][0] = min(dp[i][0], dp[j][1] + cost(j + 1, i, 0));
dp[i][1] = min(dp[i][1], dp[j][0] + cost(j + 1, i, 1));
}
}
cout << "Case #" << cas << ": " << min(dp[n][0], dp[n][1]) << endl;
}
return 0;
}
简化题意,要在原图上找一颗生成树覆盖所有边,并且每个点连它的父亲的长度乘上到根的距离的和最短。
看到数据范围,猜测它是状压 dp。考虑当前连了多少点,并且由于转移跟点到根的距离有关,我们可能会设
我们再观察树,因为是一颗树,所以点到根的距离其实就是它的深度,把树整理好它会是一层一层的,并且如果要到更远的点,前面一定会先走更近的点。树中有一个东西和距离很像,树高。它相当于表示目前最远的一些点离根的距离,或许可以用它来转移,因为当前的树高确定下来,只需要知道最后一层的节点,就可以知道代价,各个树高之间是可以转移的。
设状态
到这里,问题是,我们枚举的子集
证:设
#include<bits/stdc++.h>
using namespace std;
int n, m;
long long ans = 0x3f3f3f3f3f3f3f3f;
long long dp[15][1 << 14], f[15][15], po[15], cost[1 << 13][1 << 13];
int main(){
cin >> n >> m;
memset(f, 0x3f, sizeof(f));
for(int i = 1; i <= m; i++){
int u, v;
long long w;
cin >> u >> v >> w;
f[u][v] = f[v][u] = min(f[u][v], w);
}
po[0] = 1;
for(int i = 1; i <= n; i++){
po[i] = po[i - 1] * 2;
}
for(int i = 1; i < (1 << n); i++){
for(int j = i; j; j = (j - 1) & i){
bool flg = 0;
int now = i ^ j;
for(int p = n; p >= 1; p--){
long long minn = 0x3f3f3f3f3f3f3f3f;
if((po[p - 1] & now) == po[p - 1]){
for(int q = 1; q <= n; q++){
if((po[q - 1] & j) == po[q - 1]){
minn = min(minn, f[p][q]);
}
}
if(minn == 0x3f3f3f3f3f3f3f3f){
flg = 1;
break;
}
cost[j][i] += minn;
}
}
if(flg){
cost[j][i] = 0x3f3f3f3f3f3f3f3f;
}
}
}
memset(dp, 0x3f, sizeof(dp));
for(int i = 1; i <= n; i++) dp[1][1 << (i - 1)] = 0;
for(int k = 2; k <= n; k++){
for(int i = 1; i < (1 << n); i++){
for(int j = i; j; j = (j - 1) & i){
if(cost[j][i] == 0x3f3f3f3f3f3f3f3f) continue;
dp[k][i] = min(dp[k][i], dp[k - 1][j] + cost[j][i] * (k - 1));
}
}
}
for(int i = 1; i <= n; i++) ans = min(ans, dp[i][(1 << n) - 1]);
cout << ans << endl;
return 0;
}
Generate a String#
和入门动态规划时讲的题很像,但是它多了一种步骤,可以删除字符,并且我们分析发现,这种做法并不是无用的。
如果我们按照之前的状态
这样的
我们考虑*为什么会有删除操作,因为前面翻倍之后超过了
在思考,发现删除操作最多只会连续做一次,因为如果超过两次,为什么不在翻倍前就删呢,翻倍了删的数也翻倍,一定不优。所以直接把
Games with Rectangle#
在
首先数据范围是不允许超过
根据乘法原理,总方案数等于横列合法方案数乘上纵列合法方案数,所以我们分别求出两个的方案数再相乘就是答案。每次转移跟层数和每次的长度有关,并且我们可以把
但是即使这样复杂度还是
只是枚举的
复杂度降到
仔细思考后,发现这题既然用乘法原理简化到求横纵坐标各自的方案,其实就是在横纵坐标中各取
CF837D Round Subset#
从
这是一个选数问题,基本的状态
CF14D Two Paths#
求树中两条链,链不相交,使得两条链的长度相乘乘积最大。如果这题不是求乘积,其实就是求树的直径,设状态
而思考不相交的两条链,有的性质是,他们至少能用一条边使得它们相连,也就是说,它们之间隔着至少一条边。
那思路就有了,我们枚举他们之间隔着的边,这棵树就被分成了两棵树,分别在两棵树里面求树的直径,两个相乘就是至少隔着这条边时的最大值。依次更新
CF527D Clique Problem#
虽然这题的优解是贪心而不是
解决完这个,我们考虑状态,如果是
贪心的方法就是转换条件,可以发现
P4310 绝世好题#
选数问题,通常从左往右考虑,这题的限制不多,只需要状态
这是很容易想到的,瓶颈是复杂度是
在这个优化里,我们优化了枚举
复杂度约为
P4158 [SCOI2009]粉刷匠#
给定
所以我们可以很快求出一块木板涂色几次的最多正确格子数,设状态
于是我们就知道了每块木板各种情况下的贡献,不用担心木板内部的情况,我们现在只需要考虑怎么分配涂色次数。按顺序涂色,把每块木板抽象成一个物品,我们投入的次数越多,贡献越大,所以我们从上到下考虑,设状态
答案
不然复杂度肯定过不了,还有要注意左端点是可以和右端点重合的,也就是只涂一格,一块木板也可以一次都不涂,也要转移。
P1772 [ZJOI2006] 物流运输#
有
思考之前的状态有什么缺点,这道题并没有限制我们的更换次数,我们记录它并没有用,我们记录的初衷可能是想表示这次换不换,但是知道这一次换和不换不能顾及到整个局面,况且你不换也不一定就是你上次走的最短路。
我们重点要解决决定路线的问题,观察条件,更换路线要加钱,并且每天都得送货,这使得这
状态只需要一维,即
由于只需要单源最短路,所以直接跑
为什么可以默认前面的
#include <bits/stdc++.h>
using namespace std;
int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c - '0');
c = getchar();
}
return x * f;
}
int n, m, k, e, cnt;
bool vis[300][1100], flg[300], inq[300];
int dp[1100], h[1100], dis[300];
struct node{
int to, nxt, w;
}ed[4100];
void add(int u, int v, int w){
ed[++cnt].to = v;
ed[cnt].nxt = h[u];
ed[cnt].w = w;
h[u] = cnt;
}
int spfa(){
queue<int> q;
for(int i = 1; i <= m; i++) dis[i] = 0x3f3f3f3f, inq[i] = 0;
inq[1] = 1;
dis[1] = 0;
q.push(1);
while(!q.empty()){
int u = q.front();
q.pop();
inq[u] = 0;
for(int i = h[u]; i; i = ed[i].nxt){
int v = ed[i].to;
if(flg[v]) continue;
if(dis[v] > dis[u] + ed[i].w){
dis[v] = dis[u] + ed[i].w;
if(!inq[v]){
inq[v] = 1;
q.push(v);
}
}
}
}
return dis[m];
}
int main(){
n = read(), m = read(), k = read(), e = read();
for(int i = 1; i <= e; i++){
int u = read(), v = read(), w = read();
add(u, v, w), add(v, u, w);
}
int d = read();
for(int i = 1; i <= d; i++){
int num = read(), a = read(), b = read();
for(int j = a; j <= b; j++){
vis[num][j] = 1;
}
}
memset(dp, 0x3f, sizeof(dp));
dp[0] = -k;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++) flg[j] = 0;
for(int j = i; j >= 1; j--){
for(int k = 1; k <= m; k++) if(vis[k][j]) flg[k] = 1;
int res = spfa();
if(res == 0x3f3f3f3f) break;
dp[i] = min(dp[i], dp[j - 1] + (i - j + 1) * res + k);
}
}
cout << dp[n] << endl;
return 0;
}
P3861 拆分#
要求把
由于
题目就转化成了,在这些因数中选一些数,组成
因为
这里的
注意,这里求的是方案数,在不能降维的情况下,一定不能漏转移,也就是
复杂度约为
#include<bits/stdc++.h>
using namespace std;
long long t, n, mod = 998244353, cnt;
long long dp[8010][8010], p[8010];
int pos1[1000010], pos2[1000010];
int main(){
cin >> t;
while(t--){
cnt = 0;
cin >> n;
long long sqrn = sqrt(n);
for(long long i = 1; i * i <= n; i++){
if(n % i == 0){
p[++cnt] = i;
if(n / i != i) p[++cnt] = n / i;
}
}
sort(p + 1, p + cnt + 1);
for(int i = 1; i * 2 <= cnt + 1; i++){
pos1[p[i]] = i;
pos2[p[i]] = cnt - i + 1;
}
memset(dp, 0, sizeof(dp));
dp[1][1] = 1;
for(int i = 2; i <= cnt; i++){
for(int j = cnt; j >= 1; j--){
dp[i][j] = dp[i - 1][j];
if(j < i) continue;
if(p[j] % p[i] == 0){
long long now = p[j] / p[i];
int k = (now <= sqrn) ? pos1[now] : pos2[n / now];
dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod;
}
}
}
cout << dp[cnt][cnt] - 1 << endl;
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效