题目链接
给出一个仅含 R R 和 U U 的字符串。问如何进行一系列操作(每次操作可以将连续的 RU R U 或 UR U R 替换 D D ),使得最后得到的字符串长度最小。
本题的入手点是,先贪心地提出一个算法,再看看有没有更优的算法。
显然,我们可以提出这样的贪心算法:从左到右依次考虑字符串 s s 中相邻的字符对,一旦出现 RU R U 或者 UR U R 的组合就将其替换成 D D 。
那么,这个算法对不对呢?假如我们不这么贪心(看见可以替换的就立即替换),是否会有更优解?设字符串 s="RUR......." s =" R U R . . . . . . . " ,其中省略号表示任意长度的任意字符。我们从左到右依次考虑相邻的字符对,第一个字符对是 RU R U ,如果我不立即将 RU R U 替换成 D D ,而把 U U 留给右侧的 R R ,那么结果也没有更优,反倒是 U U 令 U U 右侧的 R R 丧失了脱单机会。所以,不贪心是不会得到更好的解的。
#include
using namespace std;
string s;
int n, cnt;
// 判断是否需要替换
bool ok(int i) {
if(s[i] == 'R' && s[i + 1] == 'U') {
return true;
}
if(s[i] == 'U' && s[i + 1] == 'R') {
return true;
}
return false;
}
int main() {
ios::sync_with_stdio(false);
cout.tie(0);
cin.tie(0);
cin >> n >> s;
// 枚举相邻字符对
for(int i = 0; i < n - 1; i++) {
if(ok(i)) {
cnt ++;
i ++;
}
}
cout << n - cnt << endl;
return 0;
}
我们将字符串 s s 打印到屏幕上。也就是说,初始状态屏幕上有一个空串 t t 。然后每次执行以下两种操作中的一种。
- 在 t t 的尾部加入某个小写英文字母
- 将 t t 拷贝一个副本 p p ,将 p p 加入 t t 的末尾(最多只能做一次)
给定 s s ,求完成目标的最少操作数。
本题的入手点在于,将问题转化为决策问题。
这个问题本质上是决策问题。正如题目介绍,我们最多可以做一次拷贝操作。这个操作完成后可能会有最优解,也可能没有。因此我们要考虑是否要做这个操作,以及这个操作在什么时候做。这就是个决策问题了,考虑到数据规模不是很大,枚举即可。
假设我们在 t t 的长度已经为 i i 的情况下做拷贝操作(此时t的各个字符是确定的,因为在拷贝之前只能做第一种操作)。如果此时做拷贝操作合法(不会得到不是 s s 的字符串),那么我们可以用 O(1) O ( 1 ) 的复杂度算出操作数,从而更新最优解。所以要求最优解就要枚举 i i 。注意,最优解的初始值应该是 s s 的长度,对应不做拷贝操作的情况。
算法整体的时间复杂度为 O(n2) O ( n 2 ) 。(除了枚举 i i 的 O(n) O ( n ) 复杂度外,判断拷贝是否合法还需要 O(n) O ( n ) 的复杂度)
#include
using namespace std;
string s;
int n, ans;
int main() {
ios::sync_with_stdio(false);
cout.tie(0);
cin.tie(0);
cin >> n >> s;
ans = n;
// 枚举在已经打印了多少字符的情况下做拷贝操作
for(int i = 0; i < n / 2; i++) {
if(s.substr(0, i + 1) == s.substr(i + 1, i + 1)) {
ans = min(ans, (i + 1) + 1 + n - 2 * (i + 1));
}
}
cout << ans << endl;
return 0;
}
有一个 x×y x × y 的矩阵,矩阵的数是按照电话按键的方式排列的。现在主角从矩阵某个位置开始连续走 n n 步(每步只能向上下左右四个方向走)。将经过的矩阵元素记录下来形成一个序列 a a 。现在给出这个 n n 和这个序列 a a ,问可能的 x x 和 y y 是多少。
本题的入手点在于按照先特殊后一般的顺序思考问题。
首先,假设矩阵只有一行。那么主角一定只能左右走。也就是说,序列中的数都是连续变化的。只会从元素 3 3 走到元素 4 4 ,从元素 15 15 走到元素 14 14 ,不会从元素 15 15 走到元素 10 10 等等。根据这个特殊情况,我们可以将所有情况分为 3 3 类:
1. 当序列中有相邻的数的数值相同的情况:相当于主角在原地踏步,这是不满足题意的。输出 NO N O
2. 当序列中所有相邻的数都是连续的,也就是说对于所有 i∈[2,n] i ∈ [ 2 , n ] ,都有 abs(a[i]−a[i−1])=1 a b s ( a [ i ] − a [ i − 1 ] ) = 1 的情况: x=1,y=maxi{a[i]} x = 1 , y = max i { a [ i ] } 肯定满足条件
3. 剩下的情况必然涉及主角的上下移动,这将会告诉我们 y y 值是多少,也就是说在有解的情况下,当 abs(a[i],a[i−1])>1 a b s ( a [ i ] , a [ i − 1 ] ) > 1 对某个 i i 成立时, abs(a[i],a[i−1]) a b s ( a [ i ] , a [ i − 1 ] ) 就等于 y y 值(如果不明白的话,画一个小矩阵,上下走走看就明白啦~)。 x x 值可以直接设置为 109 10 9 ,也可以根据 y y 值和序列中最大的值算出来。有了 x x 和 y y 值后就不难判断这是否是解了(只要遍历序列,检查是否有异常的移动即可)
#include
using namespace std;
const int maxn = 2e5 + 10;
int n, mx, mn, r, c, a[maxn];
int main() {
ios::sync_with_stdio(false);
cout.tie(0);
cin.tie(0);
cin >> n;
mx = 0;
mn = 2e9;
for(int i = 1; i <= n; i++) {
cin >> a[i];
// 预处理最大值和最小值
mx = max(mx, a[i]);
mn = min(mn, a[i]);
}
bool one = true;
for(int i = 1; i <= n - 1; i++) {
if(abs(a[i] - a[i + 1]) == 0) {
cout << "NO" << endl;
return 0;
}
if(abs(a[i] - a[i + 1]) > 1) {
// 发现行间移动,计算y
c = abs(a[i] - a[i + 1]);
one = false;
}
}
// 只有一行的情况
if(true == one) {
cout << "YES" << endl;
cout << 1 << ' ' << mx << endl;
return 0;
}
// 计算出x
r = mx / c + (mx % c > 0);
for(int i = 1; i <= n - 1; i++) {
int d = abs(a[i] - a[i + 1]);
if(d == 1) {
// 非法情况:左边界向左移动
if(a[i] % c == 0 && a[i] + 1 == a[i + 1]) {
cout << "NO" << endl;
return 0;
}
// 非法情况:在右边界向右移动
if(a[i] % c == 1 && a[i] - 1 == a[i + 1]) {
cout << "NO" << endl;
return 0;
}
}
// 非法情况:跨行移动距离与计算不符
if(d > 1 && d != c) {
cout << "NO" << endl;
return 0;
}
}
cout << "YES" << endl;
cout << r << ' ' << c << endl;
return 0;
}
有一个无向连通图,其中不含重边或者自环。给出两点 s,t s , t ,问有多少对 u,v u , v ,使得如果将 u,v u , v 连上无向边,则从 s s 到 t t 的最短路(边的长度都为 1 1 )不会变短(同时保持图中不含重边和自环)。
本题的入手点在于考察什么情况下加入无向边会使s到t的最短路变短,以及通过枚举来加强条件。
显然,在 s s 到 t t 的最短路径上,随便选取不相邻的,还不存在边的点对,就是加边后会缩短最短路径的点对。不过,这个限制条件还是有点弱(不足以确定是具体是哪些点对),看来需要手动加强限制。考虑枚举点 u u 和点 v v ,此时复杂度已经达到了 O(n2) O ( n 2 ) 。那么,是否能够在常数时间内判断出在点 u,v u , v 间加边会不会让 s s 到 t t 的最短路不变短呢?
既然要在常数时间内做一个这么强的判断,那么肯定要将一些信息预处理出来。于是预处理出两个数组 ds,dt,ds[i] d s , d t , d s [ i ] 表示从 s s 到点 i i 的最短路径长度, dt[i] d t [ i ] 表示从 t t 到点 i i 的最短路径长度。同时也与处理除了 s s 到 t t 的最短路径长度 md m d 。(用 BFS B F S 都能预处理出来)。这样在枚举到 u,v u , v 的时候,就可以算出两种最短路的长度:
1. 途经 s−>u−>v−>t s − > u − > v − > t 的最短路径长度: md0=ds[u]+1+dt[v] m d 0 = d s [ u ] + 1 + d t [ v ]
2. 途径 s−>v−>u−>t s − > v − > u − > t 的最短路径长度: md1=ds[v]+1+dt[u] m d 1 = d s [ v ] + 1 + d t [ u ]
通过比较 md m d 和 min(md0,md1) min ( m d 0 , m d 1 ) 的大小就可以知道 u,v u , v 是否是满足要求的点对了。
#include
using namespace std;
const int maxn = 1010;
bool vis[maxn];
int n, m, s, t, u, v, ans;
int G[maxn][maxn], d[2][maxn];
// BFS计算最短路
// 处理从以s为起点的数组d[idx][]
void BFS(int s, int idx) {
queue <int> que;
que.push(s);
d[idx][s] = 0;
memset(vis, 0, sizeof(vis));
vis[s] = true;
while(!que.empty()) {
int u = que.front();
que.pop();
for(int v = 1; v <= n; v++) {
if(!G[u][v] || vis[v]) {
continue;
}
que.push(v);
d[idx][v] = d[idx][u] + 1;
vis[v] = true;
}
}
}
// 判断点对是否满足要求
bool ok(int u, int v) {
if(d[0][u] + 1 + d[1][v] < d[0][t]) {
return false;
}
if(d[0][v] + 1 + d[1][u] < d[0][t]) {
return false;
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cout.tie(0);
cin.tie(0);
cin >> n >> m >> s >> t;
for(int i = 1; i <= m; i++) {
cin >> u >> v;
G[u][v] = G[v][u] = 1;
}
// 预处理
BFS(s, 0);
BFS(t, 1);
// 枚举点对
for(int i = 1; i <= n; i++) {
for(int j = i + 1; j <= n; j++) {
if(!G[i][j] && ok(i, j)) {
ans ++;
}
}
}
cout << ans << endl;
return 0;
}
有 n n 个水龙头,它们流出的水都会汇集到一个水缸里。每个水龙头有水流的水温和流量的上限两个属性。问在控制水缸温度恒为 T T 的情况下, n n 个水龙头的总流速最高可以是多少?
本题的入手点是,发现答案的单调性。
首先,这题的数学关系题目已经给得很清楚了,让我们来梳理一下。题目的限制条件是:
∑i=1nxiti∑i=1nxi=T ∑ i = 1 n x i t i ∑ i = 1 n x i = T
xi∈[0,ai] x i ∈ [ 0 , a i ]
我们要最大化的目标是( y y 是我自己引入的符号,目的是方便描述):
maxy=∑i=1nxi max y = ∑ i = 1 n x i
首先观察一下限制条件中的分式,发现它是不好化简的(至少我是这么认为的)。用几何观点的话,分子是点积的形式,但也不太好利用。
既然限制条件无法直接利用,不如反过来思考。如果限制条件满足了,会发生什么。考虑现在有一组解 (x1,x2,...,xn) ( x 1 , x 2 , . . . , x n ) 能够满足限制条件,我们怎样利用这组解来构造另一组解?答案是,我们可以将某个水温高的水龙头关小一点,再将某个水温低的水龙头开大,开大到到能够弥补前者关小所带来的的温度损失为止。调整结束后,水龙头的总流速将会增加 。
这恐怕是目前得到的最重要的信息了。因为这意味着, 如果已经有一组解,那么可能能够在增大总流速的情况下保持水缸中温度不变。但是,不能无穷地变大,因为每个 xi x i 都有 ai a i 这个上限。相反地,如果已经有一组解,那么一定能够在减小总流速的情况下保持水缸中的温度不变,直至流速无穷小 (分析方法同前一个结论一样,但是方向相反)。
总流速 y y 的这个性质使得它具有单调性,也就是说可以用二分查找来得到最优解(当然,或者无解)。于是我们可以二分 y y ,同时判断 y y 是否满足条件,也就是说判断总流速为 y y 时是否可以加权平均出 T T 的水温。
判断 y y 是否满足条件就很容易了。可以计算出能通过流速 y y 构造出的最高温度 ub u b ,以及能通过流速 y y 构造出的最低温度 lb l b (这需要实现对水龙头排序)。判断是否满足 lb≤y≤ub l b ≤ y ≤ u b 即可。
#include
using namespace std;
struct Tap {
int a, t;
// 重载小于号,便于排序
bool operator < (const Tap& o) const {
return t > o.t;
}
};
const int maxn = 2e5 + 10;
const double eps = 1e-10;
Tap taps[maxn];
int n, T, a, t;
double l, r;
// 考虑精度问题
int cmp(double x) {
if(x < -eps) {
return -1;
}
return x > eps;
}
// 判断水龙头的综流速为sum时是否能够保持水的温度为T
bool ok(double sum) {
double s = sum;
double p = sum * T;
double lb = 0, ub = 0;
// 计算上界ub
for(int i = 1; i <= n; i++) {
double a = taps[i].a;
double t = taps[i].t;
if(cmp(sum - a) >= 0) {
sum -= a;
ub += a * t;
}
else {
ub += sum * t;
break;
}
}
sum = s;
// 计算下界lb
for(int i = n; i >= 1; i--) {
double a = taps[i].a;
double t = taps[i].t;
if(cmp(sum - a) >= 0) {
sum -= a;
lb += a * t;
}
else {
lb += sum * t;
break;
}
}
return cmp(ub - p) >= 0 && cmp(p - lb) >= 0;
}
int main() {
ios::sync_with_stdio(false);
cout.tie(0);
cin.tie(0);
cin >> n >> T;
for(int i = 1; i <= n; i++) {
cin >> taps[i].a;
r += taps[i].a;
}
for(int i = 1; i <= n; i++) {
cin >> taps[i].t;
}
sort(taps + 1, taps + n + 1);
l = eps;
// 二分答案
for(int i = 1; i <= 100; i++) {
double mid = (l + r) / 2;
if(true == ok(mid)) {
l = mid;
}
else {
r = mid;
}
}
cout << setprecision(15) << fixed;
cout << l << endl;
return 0;
}
有一个 3×m 3 × m 的网格,网格中有一些障碍,其中第 k k 个障碍 (ak,lk,rk) ( a k , l k , r k ) 表示第 ak a k 行的第 lk l k 列到第 rk r k 列不能通过。问在每步只能从 (i,j) ( i , j ) 跑向 (i,j+1),(i−1,j+1) ( i , j + 1 ) , ( i − 1 , j + 1 ) 和 (i+1,j+1) ( i + 1 , j + 1 ) 的情况下,从 (2,1) ( 2 , 1 ) 跑到 (2,m) ( 2 , m ) 的方法数有多少种。
本题的入手点是,先简化问题,再逐步解决更复杂的问题。
这里的 m m 比较大,达到了 1018 10 18 。那么可能的情况是,我们可以构造出跟 m m 有关的公式或者或者跟 m m 有关的递推式(然后必然要用矩阵快速幂来加速递推)来计算答案。先有这个心理准备就行了。接下来先要无视这个数据规模,同时也无视障碍。假设 m=2 m = 2 ,并且网格中没有任何障碍。我们令 d[i][j] d [ i ] [ j ] 表示到第 i i 行第 j j 列的方法数有多少种。接着可以得到 d d 计算结果如下(此处省略取模过程):
d[1][1]=0 d [ 1 ] [ 1 ] = 0
d[2][1]=1 d [ 2 ] [ 1 ] = 1
d[3][1]=0 d [ 3 ] [ 1 ] = 0
d[1][2]=d[1][1]+d[2][1]=1 d [ 1 ] [ 2 ] = d [ 1 ] [ 1 ] + d [ 2 ] [ 1 ] = 1
d[2][1]=d[1][1]+d[2][1]+d[3][1]=1 d [ 2 ] [ 1 ] = d [ 1 ] [ 1 ] + d [ 2 ] [ 1 ] + d [ 3 ] [ 1 ] = 1
d[3][1]=d[2][1]+d[3][1]=1 d [ 3 ] [ 1 ] = d [ 2 ] [ 1 ] + d [ 3 ] [ 1 ] = 1
这里的确发现了递推关系。写成矩阵的形式有:
⎛⎝⎜d[1][i+1]d[2][i+1]d[3][i+1]⎞⎠⎟=⎛⎝⎜110111011⎞⎠⎟×⎛⎝⎜d[1][i]d[2][i]d[3][i]⎞⎠⎟ ( d [ 1 ] [ i + 1 ] d [ 2 ] [ i + 1 ] d [ 3 ] [ i + 1 ] ) = ( 1 1 0 1 1 1 0 1 1 ) × ( d [ 1 ] [ i ] d [ 2 ] [ i ] d [ 3 ] [ i ] )
于是借助矩阵快速幂技术,我们可以算出对于 m≤1018 m ≤ 10 18 的无障碍的网格的答案。现在还剩下的问题就是,如何应付障碍。其实很简单,在一段区间内,只要障碍形式的变化相同,就可以用矩阵表示转移,就可以用矩阵快速幂技术。例如网格的某列:
0 0
0 0
0 0
这就是一种障碍形式,当这列网格的右侧紧接着一列不一样的障碍形式的时候,我们就说障碍形式发生了变化,例如:
00 00
01 01
00 00
于是我们需要将网格划分成几段,使得每段中的障碍形式变化相同。例如,下面的网格( 0 0 表示空位, 1 1 表示障碍):
0010001110 0010001110
0001111110 0001111110
0010000000 0010000000
离散化后变成:
00 1 000 111 0 00 1 000 111 0
00 0 111 111 0 00 0 111 111 0
00 1 000 000 0 00 1 000 000 0
因为段内的障碍形式变化相同,一段与一段之间最多只有一次障碍形式变化。所以在每一段内用矩阵快速幂做递推,在段与段之间用矩阵快速幂做递推就可以了。
#include
using namespace std;
// 矩阵类
template <class T>
struct matrix {
vector < vector > a;
int n, m;
matrix(int n = 1, int m = 1): n(n), m(m) {
assert(n > 0 && m > 0);
a = vector < vector > (n);
for(int i = 0; i < n; i++) {
a[i] = vector (m);
}
}
matrix& operator << (const string& s) {
stringstream ss(s);
T x;
for(int i = 0; i < n * m && ss >> x; i++) {
a[i / m][i % m] = x;
}
}
T& operator () (int x, int y) {
return a[x][y];
}
// 矩阵模乘
matrix modMul(matrix &b, T mod) const {
assert(m == b.n);
matrix res(n, b.m);
for(int i = 0; i < n; i++) {
for(int j = 0; j < b.m; j++) {
for(int k = 0; k < m; k++) {
T tmp = a[i][k] * b.a[k][j] % mod;
res.a[i][j] = (res.a[i][j] + tmp) % mod;
}
}
}
return res;
}
// 矩阵快速幂
matrix modPow(T e, T mod) const {
assert(n == m);
matrix a = *this, res(n, n);
for(int i = 0; i < n; i++) {
res.a[i][i] = 1;
}
for(; e > 0; e >>= 1) {
if(e & 1) {
res = res.modMul(a, mod);
}
a = a.modMul(a, mod);
}
return res;
}
inline void set(int r, int c, string s, T x = 0) {
assert(!(r < 0 && c < 0) && !(r >= 0 && c >= 0));
stringstream ss(s);
for(int i = 0; ((c < 0 && i < m) || c >= 0 && i < n) && ss >> x; i++) {
c < 0 ? a[r][i] = x : a[i][c] = x;
}
}
};
typedef long long ll;
typedef matrix mat;
const int maxn = 1e4 + 5;
const ll mod = 1e9 + 7;
map int > mp;
int n, nn, a[maxn], s[4][4 * maxn];
ll m, b[4 * maxn], l[maxn], r[maxn];
int main() {
ios::sync_with_stdio(0);
cout.tie(0);
cin.tie(0);
cin >> n >> m;
// b用来存储所有将要被离散化的坐标
for(int i = 1; i <= n; i++) {
cin >> a[i] >> l[i] >> r[i];
b[i] = l[i] - 1;
b[n + i] = l[i];
b[2 * n + i] = r[i];
b[3 * n + i] = r[i] + 1;
}
// 必须算上边界,不然会出错
b[4 * n + 1] = 1;
b[4 * n + 2] = m;
// 排序并离散化
sort(b + 1, b + 4 * n + 3);
nn = unique(b + 1, b + 4 * n + 3) - b - 1;
// 计算原值到离散值的映射
for(int i = 1; i <= nn; i++) {
mp[b[i]] = i;
}
// 预处理前缀和数组以快速查找障碍形式
for(int i = 1; i <= n; i++) {
int id = a[i];
int lb = mp[l[i]];
int ub = mp[r[i]];
s[id][lb] ++;
s[id][ub + 1] --;
}
for(int i = 1; i <= 3; i++) {
for(int j = 1; j <= nn; j++) {
s[i][j] += s[i][j - 1];
}
}
mat A(3, 3), ans(3, 1);
A << "1 1 0 1 1 1 0 1 1";
ans << "0 1 0";
// 分段快速幂
for(int i = 2; i <= nn; i++) {
mat B = A;
for(int j = 1; j <= 3; j++) {
if(s[j][i] > 0) {
B.set(j - 1, -1, "0 0 0");
}
if(s[j][i - 1] > 0) {
B.set(-1, j - 1, "0 0 0");
}
}
ans = B.modPow(b[i] - b[i - 1], mod).modMul(ans, mod);
}
cout << ans(1, 0) << endl;
return 0;
}
城墙上有 n n 个连成一排的区域,每个区域中有一些弓箭手。弓箭手们都有 r r 的防御半径,也就是说,弓箭手能够防守到向左或向右 r r 个区域加上自己所处区域的范围。每个区域的防御等级为能够防守到该区域的弓箭手数量的总和,而城墙的防御等级为各区域防御等级的最小值。现在我们共有 k k 名备用弓箭手可以增援这 n n 个区域。问增援后城墙的防御等级的最大值能达到多少。
本题的入手点是,发现答案的单调性。
显然,有些低防御等级能够达到,高防御等级未必能够达到。这使得我们可以二分防御等级,将问题转化为判定某个防御等级是否能达到。
判定的算法为:从左到右遍历各区域,只要有区域i防御等级没有达到,就立即在相应的位置上布置弓箭手。“相应的位置”指防御半径能够够得到 i i 的最右侧的区域。
(不知道为什么这题会被放在 G G 题的位置)
#include
using namespace std;
typedef long long ll;
const int maxn = 5e5 + 10;
int n, r, a[maxn];
ll k, sum[maxn], b[maxn], c[maxn];
// 判断城墙防御等级是否能达到mn
bool ok(ll mn) {
ll tot = k;
memset(c, 0, sizeof(c));
for(int i = 1; i <= n; i++) {
c[i] += c[i - 1];
ll cur = b[i] + c[i];
if(cur < mn) {
ll need = mn - cur;
if(tot < need) {
return false;
}
tot -= need;
c[i] += need;
c[min(n + 1, i + 2 * r + 1)] -= need;
}
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cout.tie(0);
cin.tie(0);
cin >> n >> r >> k;
for(int i = 1; i <= n; i++) {
cin >> a[i];
sum[i] = sum[i - 1] + a[i];
}
// b[i]表示第i个区域的防御等级
for(int i = 1; i <= n; i++) {
int ub = min(n, i + r);
int lb = max(1, i - r);
b[i] = sum[ub] - sum[lb - 1];
}
// 二分答案
ll lb = 0;
ll ub = LLONG_MAX;
while(ub - lb > 1) {
ll mid = (lb + ub) / 2;
if(ok(mid)) {
lb = mid;
}
else {
ub = mid;
}
}
cout << lb << endl;
return 0;
}
(其它题目略)