T1 水池
题目描述
又到了一年一度的的雨季,幻想乡原来也会下雨。看着本已经干涸的池塘,灵梦想出了一个高(zhi)深(zhang)的问题:随着雨水落下,池塘中高低不平的地方会积水。给出一个n∗m大小的池塘的每个地方的高度,求雨水落下后每个地方的剩余的雨水的高度。
第一行三个数分别为n, m, L接下来n行m列共n∗m个范围在[0, L]中的整数,分别表示这个地方的高度。
输出包含n行m列,第i行第j列的数表示这个地方的积水的高度
思路
在写之前就标明,这是JZM巨佬的思路。我考试的时候打了一个垃圾代码,本来以为会全WA,结果还骗了三十分,不知道这个数据有多水所以我对出题人异常感谢,太邪恶了吧! 我们考虑一下一直往里面灌水。我们在做之前可以知道的是,最后每个点可以储存的值必定是原图中一个山的高度,没问题吧?我们就可以通过从小到大的枚举原图中山的高度去模拟水一步步上升的过程。我们现在定义一个点为嗝屁点(名字来源于JZM的一时灵感,我为了尊重代码原创人,所以在此也借用这个称呼),当且仅当水灌下去的时候它会溢出感觉在开车,但没有证据,并且它会溢出到界外。我们可以知道,每当水位上升,就会出现新的嗝屁点,我们要是发现当水位上升到某个特定高度时,我们可以这个点变为嗝屁点,那么这个水位就是这个点最后能储存的值。我们可以发现,如果我们用BFS去判断可不可以溢出到界外,我们的时间复杂度最坏为
O
(
n
2
∗
m
2
)
O(n^2*m^2)
O(n2∗m2),但是显然我们是骗不了很多分的 AC不了的,对吧?所以我们可以用并查集去维护两个点是否相通,我们这里的相通指的是它溢出后可以同样溢过另一个点,然后好像也没有什么了吧?Oh,我们可以用并查集有一个特别巧妙的地方,在于我们判断两个点在原图中的高度大小关系时因为我们是从小到大枚举高度的,所以如果挨着的两个点一个点如果fa[x]==1就说明这个点就大一些 实际上我们可以直接看原图 这道题就差不多了,对吧?
代码
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
#define Int register int
#define MAXN 1005
struct node
{
int x,y;
node(){}
node (int _x,int _y)
{
x = _x;
y = _y;
}
};
int n,m,L;
int a[MAXN][MAXN],fa[MAXN * MAXN];
vector <node> height[MAXN];
vector <int> last_ans;
int step_x[5] = {0,1,-1},step_y[5] = {0,0,0,1,-1};
int hash (int x,int y)
{
return x * m + y;
}
bool outside (int x,int y)
{
if (x < 0 || x >= n || y < 0 || y >= m) return 1;
else return 0;
}
int findSet (int x)
{
if (fa[x] < n * m && x != fa[x])
fa[x] = findSet (fa[x]);
return fa[x];
}
void unionSet (int u,int v)
{
int fa_u = findSet (u),fa_v = findSet (v);
if (fa_u != fa_v)
{
if (fa_v < n * m) fa[fa_v] = fa_u;
else if (fa_u < n * m) fa[fa_u] = fa_v;
}
return ;
}
void solve ()
{
int now = n * m;
for (Int i = 0;i <= L;++ i)
for (Int j = 0;j < height[i].size();++ j)
{
node depth = height[i][j];
fa[hash (depth.x,depth.y)] = hash (depth.x,depth.y);
for (Int k = 1;k <= 4;++ k)
{
int tx = depth.x + step_x[k],ty = depth.y + step_y[k];
if (outside (tx,ty) || (~fa[hash (tx,ty)] && findSet (hash (tx,ty)) >= n * m))
{
fa[hash (depth.x,depth.y)] = now;
break;
}
}
for (Int k = 1;k <= 4;++ k)
{
int tx = depth.x + step_x[k],ty = depth.y + step_y[k];
if (outside (tx,ty) || fa[hash(tx,ty)] == -1 || findSet (hash (tx,ty)) >= n * m) continue;
unionSet (hash (depth.x,depth.y),hash (tx,ty));
}
++ now;
last_ans.push_back(i);
}
}
void read (int &x)
{
x = 0;char c = getchar();int f = 1;
while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}
while (c >= '0' && c <= '9'){x = (x << 3) + (x << 1) + c - '0';c = getchar();}
x *= f;return ;
}
void write (int x)
{
if (x < 0){x = -x;putchar ('-');}
if (x > 9) write (x / 10);
putchar (x % 10 + '0');
}
signed main()
{
read (n),read (m),read (L);
for (Int i = 0;i < n;++ i)
for (Int j = 0;j < m;++ j)
{
read (a[i][j]);
height[a[i][j]].push_back(node (i,j));
fa[hash (i,j)] = -1;
}
solve ();
for (Int i = 0;i < n;++ i)
{
for (Int j = 0;j < m;++ j)
{
write (max (last_ans[findSet (hash (i,j)) - n * m],a[i][j]) - a[i][j]),putchar (' ');
}
putchar ('\n');
}
return 0;
}
T2 排列
题目描述
题目描述琪露诺又开始学数学了,”1+1=?“。众所周知,琪露诺无法解答这个问题,并对提出这个问题的你感到无比愤怒,于是提出了一个简单的问题,希望得到你的解答给出一个长度为n的排列,每次的操作定义为:选择一个区间[l, r],并将这个区间中的所有数变成这个区间中的最大的数,请问所有的能够通过这样的操作到达的排列有多少种呢?但是聪明的你觉得十分简单于是反问琪露诺,如果我限制总的操作次数那么答案又是多少呢?琪露诺无法回答这个问题,于是你需要给她答案。
第一行包含一个整数T表示数据的组数。对于每组数据的第一行两个整数n, K表示排列的大小和操作的次数。接下来一行n个数构成一个排列。
输出包含T行,每行输出一个答案,mod 1e9 + 7输出
思路
这道题过于难受了,我看到的时候我就知道这一定是道dp题,但实在不会打,想了半天放弃了,最后还是用了暴力MD 我们还是切入正题吧不过分停留过去,对吧 我们可以把题目转变一下,我们求k次操作后不同的结果数量实际上就是求所有可能的情况中最小操作次数小于等于k的数量,按照老套路,我们可以把这个想象成填数,感觉又像昨天的第三题 我们设
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]表示前面i个数,由前面j个数搞过来的,最小步数为k次的方案数,我们还可以设两个数组
L
i
,
R
i
{L_i},{R_i}
Li,Ri,表示原数组第i个点
L
i
R
i
L_i~R_i
Li Ri中的每个数都小于
a
i
a_i
ai,
a
i
a_i
ai为原数组的值,我们可以知道
d
p
[
i
]
[
j
]
[
k
]
+
=
d
p
[
i
]
[
j
−
1
]
[
k
]
dp[i][j][k]+=dp[i][j-1][k]
dp[i][j][k]+=dp[i][j−1][k],我们这个操作表示第j个点完全不用,一个值也不改变,这个好理解吧?如果i在j的攻击范围内,呸,
L
j
<
=
i
<
=
R
j
L_j<=i<=R_j
Lj<=i<=Rj我们还需要
d
p
[
i
]
[
j
]
[
k
]
+
=
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
(
i
!
=
j
)
]
dp[i][j][k]+=dp[i-1][j-1][k-(i!=j)]
dp[i][j][k]+=dp[i−1][j−1][k−(i!=j)],这个是为什么呢?因为如果
i
=
j
i=j
i=j的话,我们这个点就可以直接跟原数组的值相等,就可以直接转移过来,就不需要操作,否则我们操作就多一次,这个也好理解吧?我们接下来搞一个比较复杂的,我们其实
i
到
L
j
i 到L_j
i到Lj这个区间可以被j搞了,所以我们需要加上
∑
t
=
L
j
−
1
i
−
2
d
p
[
t
]
[
j
−
1
]
[
k
−
1
]
\sum_{t=L_j-1}^{i-2}dp[t][j-1][k-1]
∑t=Lj−1i−2dp[t][j−1][k−1],我们首先可以理解
k
−
1
k-1
k−1吧 我们来想一下t的范围为什么是这个,我们可以知道
i
到
L
j
i到L_j
i到Lj处在j的攻击范围内,所以我们可以把这个区间由j来进行更改赋值,所以长成这个样子,但是有同学不禁有疑惑了为什么是
L
j
−
1
到
i
−
2
L_j-1到i-2
Lj−1到i−2呢?先来讲为什么是
i
−
2
i-2
i−2,我们凭直觉感觉应该是
i
−
1
i-1
i−1,但是我们分类讨论一下,如果
i
=
j
i=j
i=j,则我们之前就已经加上了
d
p
[
i
−
1
]
[
j
−
1
]
[
k
]
dp[i-1][j-1][k]
dp[i−1][j−1][k]了,我现在要加上
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
1
]
dp[i-1][j-1][k-1]
dp[i−1][j−1][k−1]怎么可能呢?我的
i
−
1
=
j
−
1
i-1=j-1
i−1=j−1,我根本就不需要任何操作就可以完成的事情,因为这里是最小步数所以不能转移,如果
i
!
=
j
i!=j
i!=j,那我们之前就已经转移过
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
1
]
dp[i-1][j-1][k-1]
dp[i−1][j−1][k−1]岂不是多转移了一次,所以综上,是
i
−
2
i-2
i−2而不是
i
−
1
i-1
i−1,我们有些同学可能还有疑问,为什么是
L
j
−
1
L_j-1
Lj−1呢?主要是因为你看前面i转移的时候是
i
−
1
i-1
i−1而不是i,我现在在转移
L
j
L_j
Lj,就应该由
L
j
−
1
L_j-1
Lj−1来转移过来,意会了吧?确实得靠TMD的意会然后我们会发现这是
O
(
n
4
)
O(n^4)
O(n4)的dp,我们考虑进行优化,如果有人脑残要用斜率优化,平行四边形优化,单调队列优化这些的我也不拦你,毕竟是你脑残,我们实际上会发现我们在用
O
(
n
)
O(n)
O(n)计算的那个
∑
\sum
∑实际上我们可以用前缀和进行计算,从
O
(
n
)
O(n)
O(n)降到了
O
(
1
)
O(1)
O(1),所以最后我们就可以用
O
(
n
3
)
O(n^3)
O(n3)的dp通过这道题,简单吧?猛然发现自己打了这么多,MD
代码
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
#define Int register int
#define mod 1000000007
#define MAXN 205
int n,K;
int a[MAXN],L[MAXN],R[MAXN];
int dp[MAXN][MAXN][MAXN],pre[MAXN][MAXN][MAXN];
void read (int &x)
{
x = 0;char c = getchar();int f = 1;
while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}
while (c >= '0' && c <= '9'){x = (x << 3) + (x << 1) + c - '0';c = getchar();}
x *= f;return ;
}
void write (int x)
{
if (x < 0){x = -x;putchar ('-');}
if (x > 9) write (x / 10);
putchar (x % 10 + '0');
}
signed main()
{
int times;
read (times);
while (times --)
{
memset (dp,0,sizeof (dp));
memset (pre,0,sizeof (pre));
read (n),read (K);
for (Int i = 1;i <= n;++ i) read (a[i]);
for (Int i = 1;i <= n;++ i)
{
L[i] = R[i] = i;
while (L[i] > 1 && a[L[i] - 1] < a[i]) L[i] --;
while (R[i] < n && a[R[i] + 1] < a[i]) R[i] ++;
}
for (Int i = 0;i <= n;++ i)
dp[0][i][0] = pre[0][i][0] = 1;
for (Int i = 1;i <= n;++ i)
for (Int j = 0;j <= n;++ j)
for (Int k = 0;k <= K;++ k)
{
if (!j)
{
pre[i][j][k] = (pre[i - 1][j][k] + dp[i][j][k]) % mod;
continue;
}
dp[i][j][k] = dp[i][j - 1][k];
if (L[j] <= i && R[j] >= i)
{
if (k - (int)(i != j) >= 0) dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j - 1][k - (int)(i != j)]) % mod;
if (k - 1 >= 0)
{
if (i - 2 >= 0) dp[i][j][k] = (dp[i][j][k] + pre[i - 2][j - 1][k - 1]) % mod;
if (L[j] - 2 >= 0) dp[i][j][k] = (dp[i][j][k] + mod - pre[L[j] - 2][j - 1][k - 1]) % mod;
}
}
pre[i][j][k] = (pre[i - 1][j][k] + dp[i][j][k]) % mod;
}
int tot = 0;
for (Int i = 0;i <= K;++ i)
tot = (tot + dp[n][n][i]) % mod;
write ((tot % mod + mod) % mod),putchar ('\n');
}
return 0;
}
T3 方阵计数
题目描述
犬走看见千里之外的你在和EnderGZM下棋,她感到恐惧,这世界上居然还有人能和身为毒瘤的EnderGZM匹敌?他感到十分好奇,但为了遵守和大天狗大人的约定,她不能来找你。这天,终于勉强战胜了EnderGZM的你来到了她的面前。好奇的她看着你的棋盘提出了一个或许很简单的问题:对于一个n∗n的棋盘,我选择四个不同的点所能够构成的不同的正方形有多少个呢?不同的定义为:选择的四个点看成一个集合,对于每个点有唯一确定的坐标(i, j)来表示。天才的你瞬间解决了这个问题,于是进一步反问:对于每个不同的正方形,其中包含的格点的数量的和是多少呢?犬走无法解决天才的你提出的问题,你需要给出答案。
一行一个整数n表示棋盘的边长
输出一个数表示格点数的和。
思路
我们先考虑50分做法,我们用
O
(
n
2
)
O(n^2)
O(n2)做法解决50分,我们正着放的正方形的贡献我们很好求出来,即
∑
i
=
1
n
(
n
−
i
+
1
)
2
∗
(
i
+
1
)
2
\sum_{i=1}^{n}(n-i+1)^2*(i+1)^2
∑i=1n(n−i+1)2∗(i+1)2,这个我们展开之后可以用
O
(
1
)
O(1)
O(1)的时间复杂度算出来,我们再考虑斜着放的正方形做出的贡献,我们可以把它分成赵爽弦图那个样子,然后我们可以算出中间没有重复的正方形,我们再看4个小三角形,我们可以
O
(
n
)
O(n)
O(n)算出它所包含的点的个数,用相似三角形即可,主要是我太菜了,但是是可以骗过50分的,然后再用容斥原理减去重复计算过的,我们实际上可以用
O
(
1
)
O(1)
O(1)算出来,有一个小奥公式太菜了,都没见过,MD,Oh,我都不知道这篇博客我说了几个MD了我们有一个公式,即
2
∗
s
=
2
∗
a
+
b
−
2
2*s=2*a+b-2
2∗s=2∗a+b−2,其中S表示我们计算的三角形的面积,a表示三角形内的点,b表示斜边上的点,其中
b
=
(
x
,
y
)
b=(x,y)
b=(x,y),x,y,是直角三角形的直角边长度,这样我们就可
O
(
1
)
O(1)
O(1)求出来了,最后答案总和即为
∑
i
=
1
n
(
n
−
i
+
1
)
2
∗
(
i
+
1
)
2
+
∑
i
=
1
n
−
1
∑
j
=
1
n
−
i
(
n
−
i
−
j
+
1
)
2
∗
4
∗
(
i
∗
j
2
+
1
+
(
i
,
j
)
2
)
−
4
∗
(
j
+
1
)
+
(
j
−
i
+
1
)
2
\sum_{i=1}^{n}(n-i+1)^2*(i+1)^2+\sum_{i=1}^{n-1}\sum_{j=1}^{n-i}(n-i-j+1)^2*4*(\frac{i*j}2+1+\frac{(i,j)}2)-4*(j+1)+(j-i+1)^2
∑i=1n(n−i+1)2∗(i+1)2+∑i=1n−1∑j=1n−i(n−i−j+1)2∗4∗(2i∗j+1+2(i,j))−4∗(j+1)+(j−i+1)2然后我们就可以50分了,附上代码
50分代码
#pragma GCC optimize (2)
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
#define Int register int
#define int long long
#define mod 998244353
int n;
int g (int a,int b)
{
if (a > b) swap (a,b);
int ans = 0;
for (Int i = 0;i <= a;++ i)
{
int j1 = i * b / a;
ans = (ans + j1) % mod;
}
return (ans + (a + 1) % mod) % mod;
}
int G (int a,int b)
{
if (a > b) swap (a,b);
return g (a,b) * 4 - 4 * (b + 1) + (b - a + 1) * (b - a + 1);
}
int number (int len)
{
return (n - len + 1) % mod * (n - len + 1) % mod;
}
void read (int &x)
{
x = 0;char c = getchar();int f = 1;
while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}
while (c >= '0' && c <= '9'){x = (x << 3) + (x << 1) + c - '0';c = getchar();}
x *= f;return ;
}
void write (int x)
{
if (x < 0){x = -x;putchar ('-');}
if (x > 9) write (x / 10);
putchar (x % 10 + '0');
}
signed main()
{
read (n);
int tot = 0;
for (Int i = 1;i <= n;++ i)
tot = (tot + (n - i + 1) % mod * (n - i + 1) % mod * (i + 1) % mod * (i + 1) % mod) % mod;
for (Int i = 1;i <= n;++ i)
for (Int j = n - i;j >= 1;-- j)
tot = (tot + number (i + j) % mod * G (i,j) % mod) % mod;
write (tot),putchar ('\n');
return 0;
}
我们考虑把这个式子进行优化,前面一个
∑
\sum
∑我们可以直接暴力展开,然后再用公式算出来即可,考虑算后面一个
∑
\sum
∑,我们可以先把gcd提出去,然后前面我们看提出去之后的东西我们会发现它是一个5次的,我们可以%#%#%#%#%@#@!#@!$#@&&!#@%#@!%%#!@%#%!@%#@%@!%#%#@!%@!%%%%,好,这道题就这样了,实在不会正解,之后补上吧逃