SMU 2023 Spring 题单 第一周 DP
Cow Bowling
简单 dp,f[i][j]
到达第i
行第j
列的最大值
#include<iostream>
#include <vector>
#include <algorithm>
using namespace std;
#define int long long
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
int32_t main() {
int n = read();
vector<vector<int> > f(n + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++) f[i][j] = read() + max(f[i - 1][j], f[i - 1][j - 1]);
cout << *max_element(f[n].begin(), f[n].end());
return 0;
}
Sumsets
完全背包
复杂度\(O(N\log N)\)
#include<iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int mod = 1e9, N = 1e6+5;
int n , f[N];
int32_t main() {
cin >> n;
f[0] = 1;
for (int i = 1 ; i <= n ; i <<= 1 )
for (int j = i; j <= n; j++)
f[j] = (f[j] + f[j-i]) % mod;
cout << f[n];
return 0;
}
线性递推
设f[i]
表示构成i
的方案数
如果i
是奇数,则一定包含1,因为 2 的整次幂中只有 1 是奇数
如果i
是偶数则分类讨论,看是否包含 1,如果包含 1 则一定是从f[i-1]
转移过来
#include<iostream>
#include <vector>
#include <algorithm>
using namespace std;
#define int long long
const int mod = 1e9;
int32_t main() {
int n;
cin >> n;
vector<int> f(n+1 , 0 );
f[0] = 1;
for (int i = 1; i <= n; i++){
if( i & 1 ) f[i] = f[i-1];
else f[i] = (f[i-1] + f[i/2]) % mod;
}
cout << f[n] % mod;
return 0;
}
Apple Catching
f[i][j][0/1]
表示第i
秒交换j
次在0/1
树下的最优解
注意状态f[1][0][1],f[1][1][0]
是非法状态
#include <iostream>
using namespace std;
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
const int N = 1005, M = 35;
int a[N][2], f[N][M][2];
int32_t main() {
int n = read(), m = read();
for (int i = 1, x; i <= n; i++)
x = read() - 1, a[i][x] = 1;
f[1][0][0] = a[1][0], f[1][1][1] = a[1][1], f[1][0][1] = f[1][1][0] = -0x7f7f7f7f;
for (int i = 2; i <= n; i++) {
f[i][0][0] = f[i - 1][0][0] + a[i][0];
f[i][0][1] = f[i - 1][0][1] + a[i][1];
for (int j = 1; j <= min(i, m); j++) {
f[i][j][1] = max(f[i - 1][j][1], f[i - 1][j - 1][0]) + a[i][1];
f[i][j][0] = max(f[i - 1][j][0], f[i - 1][j - 1][1]) + a[i][0];
}
}
int res = 0;
for (int i = 0; i <= m; i++)
res = max(res, max(f[n][i][0], f[n][i][1]));
cout << res;
return 0;
}
Milking Time
其实这里给的时间范围是一个无用的变量\(f[i]\)表示以第\(i\)个区间结尾的最大值,则\(f[i]=\max(f[j]+efficiency_i),j\in(starting_i\ge ending_j)\)
为了方便枚举,把区间按照开始时间排序即可
注意答案是\(\max(f[i])\)
#include <iostream>
#include <vector>
#include <cstdio>
#include <climits>
#include <algorithm>
using namespace std;
#define int long long
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
struct T {
int l, r, w;
T() {};
T(int l, int r, int w) : l(l), r(r), w(w) {};
bool operator<(T b) const {
if (l != b.l) return l < b.l;
return r < b.r;
}
};
int32_t main() {
int n = read(), m = read(), t = read();
vector<int> f(m, INT_MIN);
vector<T> v(m);
for (int i = 0; i < m; i++)
v[i].l = read(), v[i].r = read() + t, v[i].w = read();
sort(v.begin(), v.end());
f[0] = 0;
for (int i = 0; i < m; i++) {
f[i] = v[i].w;
for (int j = 0; j < i; j++) {
if (v[j].r > v[i].l) continue;
f[i] = max(f[i], f[j] + v[i].w);
}
}
int res = INT_MIN;
for (int i = 0; i < m; i++) res = max(res, f[i]);
printf("%lld\n", res);
}
Cheapest Palindrome
区间 dp 的典题
首先增加字符和减少字符可以看做等价条件,所以在选择操作是,我们选择便宜的就好,对于字母\(i\)的操作代价记作v[i]
记f[l][r]
为把区间[l,r]
变成回文串的最小操作次数。
如果s[l]==s[r]
则不需要操作,直接f[l][r]=f[l+1][r-1]
反之有两种操作方法,一种是操作l
花费是f[l+1][r]+v[l]
,另一种是操作r
花费是f[l][r-1]+v[r]
两个取较小值就好。
#include <iostream>
#include <vector>
#include <cstdio>
#include <climits>
#include <algorithm>
using namespace std;
#define int long long
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
const int N = 2005;
int n, m, v[N], f[N][N];
string s;
int32_t main() {
cin >> n >> m >> s;
for (int i = 1, x, y; i <= n; i++) {
char c;
cin >> c >> x >> y;
v[c] = min(x, y);
}
for( int r = 1 ; r < m ; r ++ )
for( int l = r-1 ; l >= 0 ; l -- ){
if( s[l] == s[r] ) f[l][r] = f[l+1][r-1];
else f[l][r] = min( f[l+1][r] + v[s[l]] , f[l][r-1] + v[s[r]] );
}
cout << f[0][m-1];
return 0;
}
Coins
多重背包、完全背包
显然是一个多重背包的题目,用二进制拆分做一下就好了。但是 poj 卡常数,要把\(A_i\times C_i > m\)的情况当成完全背包做才能过。
#include <iostream>
#include <vector>
#include <cstdio>
#include <climits>
#include <algorithm>
using namespace std;
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
const int N = 1e5 + 5;
int a[N], f[N], v[N];
int32_t main() {
int n, m, tot, res;
while (true) {
n = read(), m = read();
if (n + m == 0) return 0;
for (int i = 1; i <= m; i++) f[i] = 0;
f[0] = 1, res = 0;
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1, x, t; i <= n; i++) {
x = read();
if (x * a[i] >= m) {
for( int j = a[i] ; j <= m ; j ++ )
f[j] |= f[j-a[i]];
} else {
t = 1, tot = 0;
while (x > t) v[++tot] = a[i] * t, x -= t, t <<= 1;
if (x > 0) v[++tot] = x * a[i];
for (int k = 1; k <= tot; k++)
for (int j = m; j >= v[k]; j--)
f[j] |= f[j - v[k]];
}
}
for (int i = 1; i <= m; i++) res += f[i];
printf("%d\n", res);
}
return 0;
}
多重背包、可行性分析
这里的做法是首先贪心的使用i
之前的硬币来组成j
,其次是在used
限制的情况下把多重背包转换成完全背包,一次来大幅度提升效率
#include <iostream>
#include <vector>
#include <cstdio>
#include <climits>
#include <algorithm>
#include <cstring>
using namespace std;
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
const int N = 1e5 + 5, T = 1005;
int a[N], f[N], used[N];
int32_t main() {
int n, m, res;
while (true) {
n = read(), m = read();
if (n + m == 0) return 0;
memset(f, 0, sizeof f);
f[0] = 1 , res = 0;
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1, x; i <= n; i++) {
memset(used, 0, sizeof used);
x = read();
for (int j = a[i]; j <= m; j++)
if (!f[j] && f[j - a[i]] && used[j - a[i]] < x)
f[j] = 1, used[j] = used[j - a[i]] + 1;
}
for (int i = 1; i <= m; i++) res += f[i];
printf("%d\n", res);
}
return 0;
}
Ant Counting
\(f[i][j]\)表示前\(i\)中选\(k\)个的方案数,容易可以得到\(f[i][j]=\sum_{k=0}^{\min(j,a[i])} f[i-1][j-k]\)这样的递推式,但是这个递推的求解是\(O(N^3)\)的
这样可以把\(dp\)的过程转化成\(O(N^2)\)的
#include <iostream>
#include <vector>
#include <cstdio>
#include <climits>
#include <algorithm>
#include <cstring>
using namespace std;
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
const int T = 1005, N = 100005 ,mod = 1e6;
int cnt[T], f[T][N];
int32_t main() {
int t = read(), n = read(), a = read(), b = read();
for (int x, i = 1; i <= n; i++) x = read(), cnt[x]++;
f[0][0] = 1;
for (int i = 1; i <= t; i++) {
f[i][0] = 1;
for (int j = 1; j <= n; j++) {
if( j - 1 >= cnt[i] ) f[i][j] = (f[i][j-1] + f[i-1][j] - f[i-1][j-1-cnt[i]] + mod ) % mod;
else f[i][j] = ( f[i][j-1] + f[i-1][j] ) % mod;
}
}
int res = 0;
for (int i = a; i <= b; i++)
res += f[t][i];
cout << res % mod;
return 0;
}
Dollar Dayz
完全背包,会爆long long
两种做法 __int128
#include <iostream>
#include <vector>
#include <cstdio>
#include <climits>
#include <algorithm>
#include <cstring>
using namespace std;
#define int long long
const int N = 1005;
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
void write( __int128 x ){
string s = "";
while( x > 0 )
s += char((x%10)+'0') , x /= 10;
reverse(s.begin(), s.end());
cout << s;
}
int n , m ;
__int128 f[N];
int32_t main() {
n = read() , m = read();
f[0] = 1;
for( int i = 1 ; i <= m ; i ++ )
for( int j = i ; j <= n ; j ++ )
f[j] += f[j-i];
write(f[n]);
return 0;
}
两个long long
#include <iostream>
#include <vector>
#include <cstdio>
#include <climits>
#include <algorithm>
#include <cstring>
using namespace std;
#define int long long
const int N = 1105, mod = 1e18;
int n, m, f[N], g[N];
int32_t main() {
while (scanf("%lld%lld", &n, &m) != EOF) {
memset(f, 0, sizeof f);
memset(g, 0, sizeof g);
f[0] = 1;
for (int i = 1; i <= m; i++)
for (int j = i; j <= n; j++)
g[j] = g[j] + g[j - i] + (f[j] + f[j - i]) / mod, f[j] = (f[j] + f[j - i]) % mod;
if (g[n] == 0) printf("%I64d\n", f[n]);
else printf("%I64d%018I64d\n", g[n], f[n]);
}
return 0;
}
Wooden Sticks
首先把木棍按照长度从小到大排序,然后按照宽度划分出\(x\)个不下降子序列,求\(x\)的最小值。而\(x\)的最小值就是最长下降子序列的长度。证明参考狄尔沃斯定理。
#include <cstdio>
#include <utility>
#include <vector>
#include <algorithm>
using namespace std;
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
void solve() {
int n = read();
vector<pair<int, int> > p(n);
for( int i = 0 ; i < n ; i ++ ) p[i].first = read() , p[i].second = read();
sort(p.begin(), p.end());
vector<int> f(n , 1 );
for( int i = 1 ; i < n ; i ++ ){
for( int j = 0 ; j < i ; j ++ )
if( p[j].second > p[i].second )
f[i] = max( f[i] , f[j]+1 );
}
printf("%d\n" , *max_element(f.begin(), f.end()) );
return;
}
int32_t main() {
for (int t = read(); t; t--)
solve();
return 0;
}
Bridging signals
写一个最长不上升子序列,然后 TLE 了
#include <cstdio>
#include <utility>
#include <vector>
#include <algorithm>
using namespace std;
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
void solve() {
int n = read();
vector<int> a(n), f(n, 1);
for (int i = 0; i < n; i++) a[i] = read();
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++)
if (a[j] <= a[i]) f[i] = max(f[i], f[j] + 1);
}
printf("%d\n", *max_element(f.begin(), f.end()));
return;
}
int32_t main() {
for (int t = read(); t; t--)
solve();
return 0;
}
正解就是二分优化一下,首先维护已知的最长不下降子序列a
,为了使后面的数字尽可能的放进去就要让当前的序列尽可能的小,所以在放的过程中,如果可以放到尾部就直接放,后缀去里面二分查找大于等于当前数的最小值替换即可
#include <cstdio>
#include <utility>
#include <vector>
#include <algorithm>
using namespace std;
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
void solve() {
int n = read();
vector<int> a;
for (int x, t; n; n--) {
x = read();
if (a.empty() || a.back() < x) a.push_back(x);
else {
t = lower_bound(a.begin(), a.end(), x) - a.begin();
a[t] = x;
}
}
printf("%d\n", (int) a.size());
return;
}
int32_t main() {
for (int t = read(); t; t--)
solve();
return 0;
}
Making the Grade
\(f[i][j]\)表示把前\(i\)个转换成不上升的且最后一位是\(j\)的最小花费,则\(f[i][j]=\min(f[i-1][1\cdots j]) + abs( a_i-j)\)
对于求\(\min\)的部分维护一个前缀最小值即可。然后至于不下降,我的做法是吧原数组倒过来再做一遍不上升就好了
注意这题需要对高度进行离散化。并且可以推出结论,最后的数组中的数的取值一定在最开始的数组的数值集合中
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
int read() {
int x = 0, f = 1, ch = getchar();
while ((ch < '0' || ch > '9') && ch != '-') ch = getchar();
if (ch == '-') f = -1, ch = getchar();
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
const int N = 2005;
int n, m;
int a[N], b[N], c[N];
int f[N][N], minf[N][N];
int g[N][N], ming[N][N];
int32_t main() {
n = read();
for (int i = 1; i <= n; i++) a[i] = b[i] = read();
sort(a + 1, a + 1 + n), m = unique(a + 1, a + 1 + n) - a - 1;
for (int i = 1, j = n; i <= n; i++, j--)
b[i] = c[j] = lower_bound(a + 1, a + 1 + m, b[i]) - a;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
f[i][j] = minf[i - 1][j] + abs(a[j] - a[b[i]]);
g[i][j] = minf[i - 1][j] + abs(a[j] - a[c[i]]);
if (j == 1) minf[i][j] = f[i][j], ming[i][j] = g[i][j];
else minf[i][j] = min(minf[i][j - 1], f[i][j]), ming[i][j] = min(ming[i][j - 1], g[i][j]);
}
printf("%d\n", min(minf[n][m], ming[n][m]));
return 0;
}
Space Elevator
首先根据限制高度排序,然后开始做转化成多重背包,这里因为数据范围很小可以直接做。
并且这里的状态设计呀与普通的背包不同,\(f[j]\)表示在\(i\)中积木下到达高度\(j\)能够剩下最多的积木个数,如果值是\(-1\)则代表无法到达。
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
struct node{
int h , c , limt;
bool operator < ( const node & b ) const {
return limt < b.limt;
}
};
int read(){
int ch = getchar() , x = 0;
while( ch < '0' || ch > '9' ) ch = getchar();
while( ch >= '0' && ch <= '9' ) x = (x<<3)+(x<<1)+ch-'0' , ch = getchar();
return x;
}
const int N = 405 , M = 4e4+5;
int f[M], n , m;
node a[N];
int main(){
n = read();
for( int i = 1 ; i <= n ; i ++ )
a[i].h = read() , a[i].limt = read(),a[i].c = read();
sort( a+1 , a+1+n ) , m = a[n].limt;
for( int i = 1 ; i <= m ; i ++ ) f[i] = -1;
for( int i = 1 ; i <= n ; i ++ ){
for( int j = 0 ; j <= a[i].limt ; j ++ ){
if( f[j] >= 0 ) f[j] = a[i].c;
else if ( j >= a[i].h && f[j-a[i].h] >= 1 ) f[j] = f[j-a[i].h] - 1;
}
}
for( int i = m ; i >= 0 ; i -- )
if( f[i] >= 0 ) return cout << i , 0;
return 0;
}