DP Ⅱ
一道不错的题。
策略显然是对于第i个格子的答案,由[i-R,i-L]区间内的答案转移,但是直接做的复杂度是n方的。
注意到每次区间的两个端点只会向右分别移动一格,那么我们就需要维护动态区间内的最值,容易想到单调队列。
从i=L位置开始枚举,每次维护[i-R,i-L]内的最大值,转移,这样就变成了一种线性做法。
如果i+r大于n,表示这个点下一步可以跳到对岸,那么更新答案。
但是到这里还没结束,还有下一步:初始化。
考虑到当j不可到达,那么当前位置i不能被位置j转移,只需要将所有f[i]初始化为-inf,但是0号位置一定是可以到达的,所以0号位置依旧为0。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10, inf = -300000000;
int n, l, r, a[N], f[N], q[N], ans = inf;
int main()
{
cin >> n >> l >> r;
for(int i = 0; i <= n; i ++) scanf("%d", &a[i]);
f[0] = 0;
for(int i = 1; i <= n; i ++) f[i] = inf;
int head = 1, tail = 0;
for(int i = l; i <= n; i ++)
{
while(head <= tail && f[q[tail]] <= f[i - l]) tail --;
q[++ tail] = i - l;
while(head <= tail && q[head] < i - r) head ++;
f[i] = f[q[head]] + a[i];
if(i + r > n) ans = max(ans, f[i]);
}
cout << ans << "\n";
return 0;
}
这题数据范围比较小,可以直接枚举。
用f[i][j]表示包含第i个电塔,公差为j的方案数。
注意公差可能为负,那么不妨都加上20000使其为非负数。
点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1010, H = 4e4 + 10, mod = 998244353;
int n, h[N], f[N][H];
ll ans;
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%d", &h[i]);
for(int i = 1; i <= n; i ++)
{
for(int j = 1; j < i; j ++)
{
int hh = h[i] - h[j] + 20000;
f[i][hh] += f[j][hh] + 1;
f[i][hh] %= mod;
ans += f[j][hh] + 1;
ans %= mod;
}
}
ans += n; ans %= mod;
cout << ans << '\n';
return 0;
}
根据题目表格内容,可以把题意抽象成:在一个矩阵中,每一行选取一个数,且每一行选的数的列都比上一行大。
那么问题就很简单了,用f[i][j]表示第i行选择第j个的最大美观度,由最大的f[i - 1][k]转移而来,其中1 <= k < j ,那么最大值是可以线性维护的。
因为要求统计方案,那么每次更新记录前驱即可
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int F, V, a[N][N], f[N][N], pre[N][N];
int stac[N];
int main()
{
cin >> F >> V;
for(int i = 1; i <= F; i ++)
{
for(int j = 1; j <= V; j ++)
{
scanf("%d", &a[i][j]);
}
}
for(int i = 1; i <= V - F + 1; i ++) f[1][i] = a[1][i];
int mx, now;
for(int i = 2; i <= F; i ++)
{
mx = f[i - 1][i - 1], now = i - 1;
for(int j = i; j <= V - F + i; j ++)
{
f[i][j] = f[i - 1][now] + a[i][j];
pre[i][j] = now;
if(mx < f[i - 1][j]) mx = f[i - 1][j], now = j;
}
}
mx = f[F][F], now = F;
for(int i = F + 1; i <= V; i ++)
{
if(f[F][i] > mx)
{
mx = f[F][i];
now = i;
}
}
stac[1] = now;
for(int i = 2; i <= F; i ++) stac[i] = pre[F - i + 2][stac[i - 1]];
cout << mx <<'\n';
for(int i = F; i >= 1; i --) cout << stac[i] << ' ';
return 0;
}
多重背包,完全背包结合板子,没啥好说的。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10;
int n, t[N], c[N], p[N], f[N];
int sh, sm, eh, em, m, ans;
int main()
{
scanf("%d:%d %d:%d %d", &sh, &sm, &eh, &em, &n);
m = (eh - sh) * 60 + em - sm;
for(int i = 1; i <= n; i ++)
{
scanf("%d %d %d", &t[i], &c[i], &p[i]);
}
for(int i = 1; i <= n; i ++)
{
if(p[i] == 0)
{
for(int j = t[i]; j <= m; j ++)
{
f[j] = max(f[j], f[j - t[i]] + c[i]);
}
continue;
}
for(int k = 1; k <= p[i]; k ++)
{
for(int j = m; j >= t[i]; j --)
{
f[j] = max(f[j], f[j - t[i]] + c[i]);
}
}
}
for(int i = 0; i <= m; i ++) ans = max(ans, f[i]);
cout << ans << '\n';
return 0;
}
P2340 [USACO03FALL] Cow Exhibition G
这道题让我对01背包有了新的认识,价值,体积,代价是可以灵活变通的。
在这题中,体积是奶牛个数,价值和代价是智商或者情商,为了使答案下标始终为非负数,可以整体加上某个数。
而当代价是负数时,背包的转移方程从从大到小变为从小到大。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 410;
int n, ans;
int a[N], b[N], f[800010];
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%d %d", &a[i], &b[i]);
memset(f, -0x3f, sizeof(f));
f[400000] = 0;
for(int i = 1; i <= n; i ++)
{
if(a[i] >= 0)
{
for(int j = 800000; j >= a[i]; j --)
{
f[j] = max(f[j], f[j - a[i]] + b[i]);
}
}
else
{
for(int j = 0; j <= 800000 + a[i]; j ++)
{
f[j] = max(f[j], f[j - a[i]] + b[i]);
}
}
}
for(int i = 400000; i <= 800000; i ++)
{
if(f[i] < 0) continue;
ans = max(ans, f[i] + i - 400000);
}
cout << ans << '\n';
return 0;
}
其实就是最长上升子序列,对于每个位置,分别求出从前往后和从后往前的最长上升子序列长度,并且包含这个位置自身。
最后枚举每个位置,更新答案。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, a[N], f[N], g[N], ans, ra[N], rb[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++) f[i] = g[i] = 300;
for(int i = 1; i <= n; i ++)
{
int l = 1, r = i - 1;
while(l <= r)
{
int mid = l + r >> 1;
if(f[mid] < a[i]) l = mid + 1;
else r = mid - 1;
}
f[l] = a[i]; ra[i] = l;
}
for(int i = n; i >= 1; i --)
{
int l = 1, r = n - i;
while(l <= r)
{
int mid = l + r >> 1;
if(g[mid] < a[i]) l = mid + 1;
else r = mid - 1;
}
g[l] = a[i]; rb[i] = l;
}
for(int i = 1; i <= n; i ++) ans = max(ans, ra[i] + rb[i] - 1);
ans = n - ans;
cout << ans << '\n';
return 0;
}
绝世好题!其实很简单。。。
考虑按二进制位转移,用f[i]表示最后一个数的第i位为1的最大长度,每次转移就是上一次加1.
点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int n, a[N], f[40], ans;
signed main()
{
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%lld", &a[i]);
for(int i = 1; i <= n; i ++)
{
int now = 0;
for(int j = 0; j <= 30; j ++) if(a[i] & (1 << j)) now = max(now, f[j] + 1);
for(int j = 0; j <= 30; j ++) if(a[i] & (1 << j)) f[j] = now;
}
for(int i = 0; i <= 30; i ++) ans = max(ans, f[i]);
cout << ans << '\n';
return 0;
}
这是我做dp系列暂时遇到过最恶心的题了,要注意很多细节。
用f[i][j]表示到第i个数,加到j所需要的最少加号数。那么每次枚举上一次的加号再转移就行了。
细节见代码。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 50, M = 1e6 + 10;
int n, m, f[N][M], sum[N][N], chk[N][N];
char s[N];
int check(int x, int y)
{
int w = y - x + 1, res = 0;
for(int i = x; i <= y; i ++)
{
if(s[i] == '0') res ++;
else break;
}
chk[x][y] = (w - res > 6);
if(w - res > 6) return 1;
else return 0;
}
int main()
{
scanf("%s", s + 1);
cin >> m;
n = strlen(s + 1);
for(int i = 0; i <= n; i ++)
for(int j = 0; j <= 100000; j ++)
f[i][j] = -1;
for(int i = 1; i <= n; i ++)
{
for(int j = i; j <= n; j ++)
{
if(check(i, j - 1)) continue;
sum[i][j] = sum[i][j - 1] * 10 + (s[j] - '0');
}
}
for(int i = 1; i <= n; i ++)
{
if(chk[1][i]) break;
f[i][sum[1][i]] = 0;
}
f[0][0] = 0;
int x, y;
for(int i = 2; i <= n; i ++)
{
for(int j = 0; j <= m; j ++)
{
for(int k = 1; k < i; k ++)
{
if(chk[k][i]) continue;
x = j - sum[k + 1][i]; if(x < 0) continue;
y = f[k][x]; if(y == -1) continue;
if(f[i][j] == -1 || f[i][j] > y + 1) f[i][j] = y + 1;
}
}
}
cout << f[n][m] << '\n';
return 0;
}
有点思维的题。
用f[i][j]表示a串的前i个与b串的前j个匹配的最小操作次数。
f[i][j]只有可能由f[i][j - 1],f[i - 1][j], f[i - 1][j - 1]之一加一转移而来,特别地,当a[i] = b[j],直接由f[i - 1][j - 1]转移而来。
其实比较好证,如果f[i][j]不由f[i - 1][j]或f[i][j - 1]加一转移而来,那么说明有更优的操作方案,而这个方案只有可能是此时f[i][j]a串第i个和b串第j个相同(或者能接着a串已有的继续和b串匹配),而这种情况我们已经特判了,f[i - 1][j - 1]同理。
这个确实如果不看题解有点难想。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2010;
char a[N], b[N];
int f[N][N], la, lb;
int main()
{
scanf("%s %s", a, b);
la = strlen(a);
lb = strlen(b);
for(int i = 1; i <= la; i ++) f[i][0] = i;
for(int i = 1; i <= lb; i ++) f[0][i] = i;
for(int i = 1; i <= la; i ++)
{
for(int j = 1; j <= lb; j ++)
{
if(a[i - 1] == b[j - 1])
{
f[i][j] = f[i - 1][j - 1];
continue;
}
f[i][j] = min(min(f[i - 1][j], f[i][j - 1]), f[i - 1][j - 1]) + 1;
}
}
cout << f[la][lb] << '\n';
return 0;
}
与《编辑距离》那题很像,思路都是依赖长度小1的串。
这道题用f[i][j]表示把第i个到第j个变为回文串的最小插入次数。
如果s[i] == s[j],那么f[i][j] = f[i + 1][j - 1],否则f[i][j] = min(f[i + 1][j], f[i][j - 1]) + 1。
上面是解法一。
接下来分享一个别人的很秒的解法。
我们对原序列进行倒置,求整个序列变为回文串的最小操作次数,显然是对原来子序列进行插入,这个最优子序列就是原序列中满足回文的最大长度子序列,那么我们对原序列和倒置序列求一个最长公共子序列就是那个最优子序列。
以下附解法一代码。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, f[N][N];
char s[N];
int main()
{
scanf("%s", s + 1);
n = strlen(s + 1);
memset(f, 0x3f, sizeof(f));
for(int i = 1; i <= n; i ++) f[i][i] = 0;
for(int i = 1; i <= n; i ++)
{
for(int j = i - 1; j >= 1; j --)
{
if(s[j] == s[i])
{
f[j][i] = f[j + 1][i - 1];
if(i == j + 1) f[j][i] = 0;
continue;
}
f[j][i] = min(f[j + 1][i], f[j][i - 1]) + 1;
}
}
cout << f[1][n] << '\n';
return 0;
}
由于涉及很多式子有些麻烦,并且本人不会标准格式,故选择了一篇不错的题解。
这是本人代码。
点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n, m, kk, f[N][N] = {1}, sum[N][N];
char a[N], b[N];
signed main()
{
cin >> n >> m >> kk;
scanf("%s", a + 1);
scanf("%s", b + 1);
for(int i = 1; i <= n; i ++)
{
for(int j = m; j >= 1; j --)
{
for(int k = kk; k >= 1; k --)
{
f[j][k] = f[j][k] + (sum[j][k] = (a[i] == b[j] ? sum[j - 1][k] + f[j - 1][k - 1] : 0));
f[j][k] %= mod;
}
}
}
cout << f[m][kk] << '\n';
return 0;
}
这道题是P3146 [USACO16OPEN] 248 G的变式。
原版可以n^3跑区间dp,这道题数据加强了,但是并不麻烦。
用f[i][j]表示以j为左端点,合成i的右端点加一。
那么f[i][j] = f[i - 1][f[i - 1][j]]。
这体现了倍增的思想。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;
int n, ans;
int a[N], f[70][N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]), f[a[i]][i] = i + 1;
for(int i = 2; i <= 58; i ++)
{
for(int j = 1; j <= n; j ++)
{
if(!f[i][j]) f[i][j] = f[i - 1][f[i - 1][j]];
if(f[i][j]) ans = i;
}
}
cout << ans << '\n';
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】