省选测试14
T1
你有一个字符串 (S_0),想要用它造出共 n 个新字符串 (S_{0},cdots,S_{n-1})。
你会按顺序进行 n-1 次操作,造出新字符串。第 i 次操作有两种方法:
S x l r 截取 (S_x) 的 [l,r) 子串,即第 l 到第 r-1 个字符作为 (S_i)
A x y 将 (S_x) 和 (S_y) 拼接作为 (S_i)
最后,你会关心 (S_{n-1}) 长什么样,因此请输出 (S_{n-1}) 所有字符的 ASCII 码之和,结果对 (10^9+7)取模。
本题中字符串下标均从 0 开始编号,且保证所有字符串串长不超过 (2^{63}-1),并且所有操作均合法(即(0leq x,y<i,0leq l < rleq Len(S_x))
对于全部数据,(nleq 2000, 1leq Len(S_0)leq 2000),同时满足题目描述中给的限制。
搜索 + 记忆化.
md传参类型传错了少了40pts...
直接分情况搜索就好了, 记录一个第一个串的前缀和.
不过我写的记搜比较慢, 我只记录了一个第(i)个字符串的整个的价值是多少, 但是题解记录的是第(i)个字符串前(l)个字符的价值是多少, 可能用的空间大了点(但这题不卡空间).
#include <bits/stdc++.h>
#define ull unsigned long long
using namespace std;
inline ull read() {
ull s = 0, f = 1; char ch;
while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
return s * f;
}
const int N = 2005, mod = 1e9 + 7;
int n;
int sum[N], ans[N];
ull len[N];
char ch[N];
struct ques { int opt, x; ull l, r; } q[N];
int calc_qj(int x, ull l, ull r) {
if(x == 0) return (sum[r] - sum[l - 1] + mod) % mod;
if(r - l + 1 == len[x]) return ans[x];
if(q[x].opt == 2) {
if(r <= len[q[x].l]) return calc_qj(q[x].l, l, r);
else if(l > len[q[x].l]) return calc_qj(q[x].r, l - len[q[x].l], r - len[q[x].l]);
else return (calc_qj(q[x].l, l, len[q[x].l]) + calc_qj(q[x].r, 1, r - len[q[x].l])) % mod;
}
if(q[x].opt == 1) {
return calc_qj(q[x].x, l + q[x].l - 1, r + q[x].l - 1);
}
}
int calc(int x) {
if(ans[x]) return ans[x];
// cout << x << " " << q[x].l << " " << q[x].r << "
";
if(q[x].opt == 2) {
ans[x] = (calc(q[x].l) + calc(q[x].r)) % mod;
len[x] = len[q[x].l] + len[q[x].r];
}
else {
ans[x] = calc_qj(q[x].x, q[x].l, q[x].r);
len[x] = q[x].r - q[x].l + 1;
// cout << x << ":" << q[x].r << " " << q[x].l << "
";
}
return ans[x];
}
int main() {
// freopen("string.in","r",stdin); freopen("string.out","w",stdout);
scanf("%d", &n);
scanf("%s", ch + 1);
int len_ch = strlen(ch + 1);
for(int i = 1;i <= len_ch; i++) sum[i] = (sum[i - 1] + ch[i]) % mod;
ans[0] = sum[len_ch]; len[0] = len_ch;
// for(int i = 1;i <= len_ch; i++) cout << i << ":" << sum[i] << "
";
for(int i = 1;i < n; i++) {
char ch_; cin >> ch_;
if(ch_ == 'S') {
q[i].opt = 1; q[i].x = read(); q[i].l = read() + 1; q[i].r = read();
}
if(ch_ == 'A') {
q[i].opt = 2; q[i].l = read(); q[i].r = read();
}
}
for(int i = 1;i < n - 1; i++) {
calc(i);
// cout << i << ":" << ans[i] << " " << len[i] << "
";
}
printf("%d
", calc(n - 1));
fclose(stdin); fclose(stdout);
return 0;
}
T2
你有 n 株植物,高度为 (a_i)。你希望让它们的高度看起来不太相同,具体来说:任意两株高度差的绝对值不小于 d。
每次你可以选择一株植物,将其拔高或压低 1,这花费你 1 的力气,这个操作可以进行任意多次。注意,植物高度不能是负的,即你需要保证任意时刻 (a_igeq 0)。
你最少需要使用多少力气,才能使得植物高度看起来不太相同呢?
多组测试数据。第一行一个整数 T 为数据组数,接下来每组格式如下:
第一行两个整数 n,d。
接下来一行 n 个整数 (a_i)。
对于全部数据,(sum nleq 3cdot 10^5,1leq dleq 10^6, 0leq a_ileq 10^{11})
开始想了了费用流的做法, 骗了60pts.
就是先给每个(a_i)排序, 然后求出所有 (x = a_{i+1} - a_i - d), 对于(x)小于0的就从源点向他连一条流量为(abs(x)), 费用为0的边, 对于大于0的就从他向汇点连一条流量为(x), 费用为0的边. 然后这些点之间都连双向的流量为(inf), 费用为1的边. 为啥呢?
如果说提高一个(i)的高度的话, (i-1)和(i)的高度差就会增加1, (i+1)和(i)的高度差就会减小1, 然后又花了1的体力. 都减(d)是因为要保证相邻两个的差值大于等于d.
但是还有特殊的两个节点, 就是最小的那个可以压到0, 最大的那个可以无限高, 于是再连两条特殊的边就好了 : 从0向汇点连一条流量为(a_1), 费用为0的边, 从(n+1)向汇点连一条流量为(inf), 费用为0的边.
#include <bits/stdc++.h>
using namespace std;
inline long long read() {
long long s = 0, f = 1; char ch;
while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
return s * f;
}
const int N = 4e5, M = 3e6;
const long long inf = 1e18, Inf = 1e9;
int s, t, n, d_, cnt;
int d[N], pre[N], las[N], head[N];
long long b[N], a[N], incf[N];
struct edge { int v, to, nxt; long long c; } e[M];
void add(int x, int y, long long z1, int z2) {
// cout << x << " " << y << " " << z1 << " " << z2 << "!!!!
";
e[++ cnt].nxt = head[x]; head[x] = cnt; e[cnt].to = y; e[cnt].c = z1; e[cnt].v = z2;
e[++ cnt].nxt = head[y]; head[y] = cnt; e[cnt].to = x; e[cnt].c = 0; e[cnt].v = -z2;
}
int bfs() {
for(int i = s;i <= t; i++) d[i] = Inf, incf[i] = inf;
queue <int> q; d[s] = 0; pre[t] = -1; q.push(s);
while(q.size()) {
int x = q.front(); q.pop();
for(int i = head[x]; i ; i = e[i].nxt) {
int y = e[i].to;
if(e[i].c && d[y] > d[x] + e[i].v) {
d[y] = d[x] + e[i].v; q.push(y);
incf[y] = min(incf[x], e[i].c);
pre[y] = x; las[y] = i;
}
}
}
return pre[t] != -1;
}
void Work() {
long long res = 0;
while(bfs()) {
int now = t;
res += 1ll * d[t] * incf[t];
// cout << d[t] << " " << incf[t] << "!!!!!
";
while(now != s) {
e[las[now]].c -= incf[t];
e[las[now] ^ 1].c += incf[t];
now = pre[now];
}
}
printf("%lld
", res);
}
void Clear() {
cnt = 1; s = 0; t = n + 2;
memset(head, 0, sizeof(head));
}
void Init() {
n = read(); d_ = read();
Clear();
for(int i = 1;i <= n; i++) a[i] = read();
sort(a + 1, a + n + 1);
for(int i = 2;i <= n; i++) b[i] = a[i] - a[i - 1] - d_;
add(1, t, a[1], 0);
for(int i = 2;i <= n; i++)
if(b[i] < 0) add(s, i, -b[i], 0);
else add(i, t, b[i], 0);
add(n + 1, t, inf, 0);
for(int i = 1;i <= n + 1; i++) {
if(i != 1) add(i, i - 1, inf, 1);
if(i != n + 1) add(i, i + 1, inf, 1);
}
}
int main() {
freopen("different.in","r",stdin); freopen("different.out","w",stdout);
for(int T = read(); T ; T --) {
Init(); Work();
}
fclose(stdin); fclose(stdout);
return 0;
}
/*
2
4 10
1 100 7 10
5 1
1 1 1 1 1
1
5 1
1 1 1 1 1
*/
接下来说说正解吧.
正解是DP.其实我知道该用DP写这道题, 但是我不会qwq.
首先有一个经典的转化 : (A_i = a_i - (i-1)*d). 那么问题就转化成了将(A_i)序列变成单调不降序列的最小代价.(注意(a_i)是排过序的)
设(f_{i,j})表示前(i)个元素转化成单调不降序列, 并且(A_i = j)的最小代价.那么可以列出转移方程 :
怎么优化呢? 我们可以考虑一下它的函数图像是什么, 我们如果可以找到它的最低点就好了.
先画一下(f_{1,j} = |A_1 - j|)的图像 : 就是普通的绝对值函数图像, 没有啥好画的.
然后在考虑一下(f_{2,j} = |A_2 - j| + min{ f_{1, -infty-j} })的图像是什么, 就是一段绝对值函数和刚刚(f_1)的前缀最小值函数合并一下, 类似一个下凸壳.
最后(f_{n,j})的图像也是一个下凸壳, 并且斜率是连续的, 0, -1, -2, (dots), -k.
最后的答案就是(min{ f_{n,j} }), 我们需要找到函数图像最低的点在哪里.
设(s_i)是下凸壳上定点的横坐标, 并且从大到小排序.
那么最后的答案就是(f_{n,0} - sum_{i=1}^{n-1} i * (s_i-s_{i-1})), (i)就是那个斜率嘛, 是连续的.
现在就考虑(f_{n_0})怎么求就好了. 因为要求(f_{n,0}), 所以(A_n = 0), 又因为(A_1 = a_1 geq 0), 所以整个(A_i)序列一定都是0, 因为要保证单调不降, 那么很自然地, (f_{n,0} = sum_{i=1}^{n} A_i).
具体的维护这个图像的拐点要用到堆, 看下代码就好了.
#include <bits/stdc++.h>
using namespace std;
inline long long read() {
long long s = 0, f = 1; char ch;
while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
return s * f;
}
const int N = 3e5 + 5;
long long a[N], s[N];
priority_queue <long long> q;
int main() {
for(int T = read(); T ; T --) {
int n = read(), d = read();
for(int i = 1;i <= n; i++) a[i] = read();
sort(a + 1, a + n + 1);
for(int i = 1;i <= n; i++) {
long long x = max(0ll, a[i] - 1ll * (i - 1) * d);
q.push(x); q.push(x); q.pop();
}
int cnt = 0;
long long res = 0;
for(int i = 1;i <= n; i++) res += abs(a[i] - 1ll * (i - 1) * d);
while(q.size()) s[++ cnt] = q.top(), q.pop(); s[++ cnt] = 0;
for(int i = 1;i < cnt; i++) res -= (s[i] - s[i + 1]) * i;
printf("%lld
", res);
}
return 0;
}
T3
你有 2n+m 根绳子,每根绳子两端染有黑、白两种颜色。
其中 n 根绳子两端都是黑色; n 根绳子两端都是白色; m 根绳子一段黑色另一端白色。
现在绳子一端为黑色和白色的线头分别有 2n+m 个,你想要把黑色和白色一端的线头随机配对(共 (2n+m)! 种方法),每对配对的线头用胶水拼接起来。可以发现,这样所有绳子一定会绕成若干个环。
你想要求出环的个数期望是多少,结果对 (10^9+7) 取模。
一行两个整数 n,m。
对于全部数据,(0leq n,mleq 10^6)。
一看期望就头疼啊md.
直接设(f_{n,m})是两端都为0的线段有(n)条, 两端都为1的线段有(m)条, 一端为0一端为1的有(m)条的期望答案是多少. 最后输出(f_{n,m})就好了.
然后分情况讨论, 我们拿出一条一端是0, 一端是1的来和别的组合 :
1.和两端都是0或两端都是1的连接, 那么连完之后会得到两端都是1或两端都是0的(注意顺序), 并没有形成环, 这样的一共有(2n)种方案.
2.和一端为1一端为0的连(不包括自己), 连完之后会得到一端为0一端为1的, 并没有形成环, 这样的一共有(m-1)种方案.
3.自己首尾相连, 连完之后会得到一个环, 这样的一共有(1)种方案.
那么总共有(2n+m)中方案, 有贡献的只有1个, 那么可以得到转移方程 :
如果说我们知道了(f_{n,0})的话就可以(O(n))求答案了.
两端为0的只能和两端为1的去连, 因为没有一端是0一端是1的了, 再结合上面那个式子就可以得到 :
也是可以(O(n))求的.
代码非常短 :
#include <bits/stdc++.h>
using namespace std;
inline long long read() {
long long s = 0, f = 1; char ch;
while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
return s * f;
}
const int N = 1e6 + 5, mod = 1e9 + 7;
int n, m, ans;
int ksm(int x, int y) {
int res = 1;
while(y) { if(y & 1) res = 1ll * res * x % mod; x = 1ll * x * x % mod; y >>= 1; }
return res;
}
int main() {
n = read(); m = read();
for(int i = 0;i < n; i++) ans = (ans + ksm(2 * i + 1, mod - 2)) % mod;
for(int i = 1;i <= m; i++) ans = (ans + ksm(2 * n + i, mod - 2)) % mod;
printf("%d", ans);
return 0;
}