雅礼学习10.1
雅礼学习10.1
上午考试解题报告
各题状况
每道题的难度大概都在noip提高组Day2T2T3难度
T1
只会模拟
模拟写完之后,小小加了一个优化
期望得分20+20
T2
光是理解题意就花了好长时间。。。
然后勉勉强强算是会了暴力的写法(复杂度\(O(N!)\))
不会判重,但是lyq说按照他的想法这题根本不需要判重???
T3
我。。。这题彻底理解错误了
四联通是指对于每个格子向外扩展的方向
而实际主角可以每次对一个联通块进行操作使其变色
还以为每次只能处理特定形状的部分。。。。
GG
各题代码
T1
/*
* 考虑根据题意模拟
* 对每次询问都进行[l,r]内的mod操作,取mod后最大值
* 大概有20分
*/
#include <cstdio>
#include <algorithm>
inline int read()
{
int n=0,w=1;register char c=getchar();
while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
while(c>='0'&&c<='9')n=n*10+c-'0',c=getchar();
return n*w;
}
inline int max(int x,int y)
{return x>y?x:y;}
const int N=1e5+1;
int n,m,a[N];//,ans[N];
/*struct Query{
int l,r,k,id;
bool operator<(const Query &x)const
{return k<x.k;}
}que[N];*/
int main()
{
freopen("flower.in","r",stdin);
freopen("flower.out","w",stdout);
n=read(),m=read();
for(int i=1;i<=n;++i)
a[i]=read();
for(int ans,l,r,k,i=1;i<=m;++i)
{
ans=0;
l=read(),r=read(),k=read();
for(;l<=r;++l)
ans=max(ans,(a[l]>=k?a[l]%k:a[l]));
printf("%d\n",ans);
}
fclose(stdin);fclose(stdout);
return 0;
}
T2
/*
* 尝试理解题意
* 。。。
* 理解无能。。。
*
* 等会好像是dfs的方式
* 判重不会搞。
* 那就不搞了.
*/
#include <stack>
#include <cstdio>
inline int read()
{
int n=0,w=1;register char c=getchar();
while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
while(c>='0'&&c<='9')n=n*10+c-'0',c=getchar();
return n*w;
}
const int N=6001,mod=1e9+7;
int n,ans,x[N],y[N],lx[N],topx,ly[N],topy;
void dfs()
{
for(int i=1;i<=n;++i)
if(y[i]<ly[topy] && (x[i]<lx[topx] && x[i]>lx[topx-1])
|| (x[i]>lx[topx] && x[i]<lx[topx-1]))
{
ly[++topy]=y[i];
lx[++topx]=x[i];
dfs();
--topx,--topy;
}
++ans;
if(ans==mod)ans=0;
}
void dfsbegin()
{
for(int i=1;i<=n;++i)
if(y[i]<ly[topy])
{
ly[++topy]=y[i];
lx[++topx]=x[i];
dfs();
--topx,--topy;
}
++ans;
}
int main()
{
freopen("refract.in","r",stdin);
freopen("refract.out","w",stdout);
n=read();
for(int i=1;i<=n;++i)
x[i]=read(),y[i]=read();
for(int i=1;i<=n;++i)
{
ly[++topy]=y[i];
lx[++topx]=x[i];
dfsbegin();
--topx,--topy;
}
printf("%d",ans);
fclose(stdin);fclose(stdout);
return 0;
}
T3
/*
* 谁能告诉我样例是怎么出来的。。。
*/
#include <cstdio>
#include <cstring>
const int N=51,dx[]={0,1,0,-1},dy[]={1,0,-1,0};
int r,c,ans,sum,color[N][N];
bool map[N][N];
void dfs(int x,int y)
{
color[x][y]=sum;
for(int xx,yy,i=0;i<4;++i)
{
xx=x+dx[i],yy=y+dy[i];
if(xx<1 || xx>r || yy<1 || yy<c)continue;
dfs(xx,yy);
}
}
void work()
{
for(int i=1;i<=r;++i)
for(int j=1;j<=c;++j)
if(true);
}
int main()
{
freopen("paint.in","r",stdin);
freopen("paint.out","w",stdout);
scanf("%d%d",&r,&c);
int tot=0;
for(int x,i=1;i<=r;++i)
for(int j=1;j<=c;++j)
{
scanf("%d",x);
map[i][j]=x;
if(!x)++tot;
}
/*if(r<=15 && c<=15)
{
for(int i=1;i<=r;++i)
for(int j=1;j<=c;++j)
if(!color[i][j])
{
++sum;
dfs(i,j);
}
}
else*/
if(r==1)
{
for(int i=1;i<=c;++i)
if(map[1][i]!=map[1][i-1] && map[1][i]!=map[1][i+1])
map[1][i]=map[1][i-1],++ans;
for(int i=1;i<=c;++i)
if(map[1][i])
++++i,++ans;
printf("%d",ans);
}
else printf("%d",tot>4?tot>>1:tot);
fclose(stdin);fclose(stdout);
return 0;
}
正解及代码
T1
\(20\)分\(n^2\)做法
\(40\)分块
另外\(20\)分的做法求前缀和,查看某个数字是否存在
当\(k\)比较大的时候就主席树,当\(k\)比较小的时候因为主席树复杂度会爆,用上面另\(20\)分做法
真·正解:
考虑当\(k\)确定的时候如何求解,显然对于所有形如\([a_k,(a+1)k)\)的值域,最大值一定是最优的
进一步观察发现,这样的区间总数只有\(k\times \ln k\)个,考虑分块,那么我们可以在\(O(n+k\ln k)\)的时间复杂度内处理出一个块对于任意\(k\)的答案,询问的时候复杂度是\(O(mS),(S\text{是块的大小})\)的,取\(S=\sqrt{k\ln k}\)可以达到最优复杂度\(O(n\sqrt{k\ln k})\)
#include <bits/stdc++.h>
using std::pair;
using std::vector;
using std::string;
typedef long long ll;
typedef pair<int, int> pii;
#define fst first
#define snd second
#define pb(a) push_back(a)
#define mp(a, b) std::make_pair(a, b)
#define debug(...) fprintf(stderr, __VA_ARGS__)
template <typename T> bool chkmax(T& a, T b)
{ return a < b ? a = b, 1 : 0; }
template <typename T> bool chkmin(T& a, T b)
{ return a > b ? a = b, 1 : 0; }
const int oo = 0x3f3f3f3f;
string procStatus() {
std::ifstream t("/proc/self/status");
return string(std::istreambuf_iterator<char>(t),std::istreambuf_iterator<char>());
}
template <typename T> T read(T& x) {
int f = 1; x = 0;
char ch = getchar();
for(;!isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
for(; isdigit(ch); ch = getchar()) x = x * 10 + ch - 48;
return x *= f;
}
const int B = 1000;
const int N = 100000;
int n, q;
int a[N + 5];
int lst[N + 5];
int ans[105][N + 5];
int main() {
//freopen("flower.in", "r", stdin);
//freopen("flower.out", "w", stdout);
read(n); read(q);
for(int i = 0; i < n; ++i) read(a[i]);
int blks = (n-1) / B + 1;
for(int i = 0; i < blks; ++i) {
memset(lst, 0, sizeof lst);
for(int j = i * B; j < (i+1) * B && j < n; ++j)
lst[a[j]] = a[j];
for(int j = 1; j <= N; ++j)
chkmax(lst[j], lst[j-1]);
for(int j = 1; j <= N; ++j)
for(int k = 0; k <= N; k += j)
chkmax(ans[i][j], lst[std::min(k + j - 1, N)] - k);
}
while(q--) {
static int l, r, k;
read(l), read(r), read(k);
-- l, -- r;
int x = (l / B) + 1, y = (r / B), res = 0;
if(x == y + 1) {
for(int i = l; i <= r; ++i) chkmax(res, a[i] % k);
} else {
for(int i = x; i < y; ++i) chkmax(res, ans[i][k]);
for(int i = l; i < x*B; ++i) chkmax(res, a[i] % k);
for(int i = r; i >=y*B; --i) chkmax(res, a[i] % k);
}
printf("%d\n", res);
}
return 0;
}
T2
10分和20分其实都是\(n^3\)做法,区别仅在于常数问题
50分是\(n^2\)的\(dp\),但是因为空间开不下就变成了\(50\)分
100分:上面50分的dp把空间卡成\(\frac{n^2}{2}\)就是100分了
\(dp[i][j]\)表示上一条折线是\(i\rightarrow j\)的
具体地讲:
若将所有点按照\(y_i\)的顺序进行转移,有上界和下界两个限制,优化比较难
那么考虑按照\(x_i\)排序进行转移,并且记录\(f_{i,0/1}\)表示以第\(i\)个点为顶端接下来向左或者向右的折线方案数,从左到右加点
考虑前\(i\)个点构成的包含\(i\)点的折线,由于新加入的点横坐标是当前考虑的点钟横坐标最大的,所以只可能是折线的起始点或者第二个点
如图,假设新加入点\(i\)是折线的起始点,\(k\)是一个符合转移条件的点(也就是在\(i\)点下方),显然可行
如图,\(i\)是当前点,\(j\)是上一个点,\(k\)是一个符合条件的点(在\(i\)点左下方,同时在\(j\)点右下方)
那么有
- \(\forall y_j\lt y_i,f_{i,0}\leftarrow f_{j,1}\)
- \(\forall y_j\gt,f_{j,1}\leftarrow f_{k,0} | x_k\gt x_j\; and\; y_k\lt y_i\)
第二种情况可以进行前缀和优化,复杂度\(O(n^2)\)
#include <bits/stdc++.h>
using std::pair;
using std::vector;
using std::string;
typedef long long ll;
typedef pair<int, int> pii;
#define fst first
#define snd second
#define pb(a) push_back(a)
#define mp(a, b) std::make_pair(a, b)
#define debug(...) fprintf(stderr, __VA_ARGS__)
template <typename T> bool chkmax(T& a, T b)
{ return a < b ? a = b, 1 : 0; }
template <typename T> bool chkmin(T& a, T b)
{ return a > b ? a = b, 1 : 0; }
const int oo = 0x3f3f3f3f;
string procStatus() {
std::ifstream t("/proc/self/status");
return string(std::istreambuf_iterator<char>(t),std::istreambuf_iterator<char>());
}
template <typename T> T read(T& x) {
int f = 1; x = 0;
char ch = getchar();
for(;!isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
for(; isdigit(ch); ch = getchar()) x = x * 10 + ch - 48;
return x *= f;
}
const int N = 6000;
const int mo = 1e9 + 7;
pii p[N + 5];
int dp[N + 5][2], n;
int main() {
//freopen("refract.in", "r", stdin);
//freopen("refract.out", "w", stdout);
read(n);
for(int i = 1; i <= n; i++) {
read(p[i].fst), read(p[i].snd);
}
sort(p + 1, p + n + 1);
for(int i = 1; i <= n; i++) {
dp[i][0] = dp[i][1] = 1;
for(int j = i-1; j >= 1; j--) {
if(p[j].snd > p[i].snd) {
(dp[j][1] += dp[i][0]) %= mo;
}else {
(dp[i][0] += dp[j][1]) %= mo;
}
}
}
int ans = mo - n;
for(int i = 1; i <= n; i++)
ans = ((ans + dp[i][0]) % mo + dp[i][1]) % mo;
printf("%d\n", ans);
return 0;
}
T3
7分输出黑色联通块个数
26分状压DP
民间100分做法:每个联通块和它相邻的联通块连边,最后发现是一个图,如果每个联通块只能在图中出现一次,就会变成一棵树
然后找最长链
正解做法:
可以发现一个“比较显然”的结论:存在一种最优方案使得每次操作的区域是上一次的子集且颜色和上一次相反
考虑归纳法证明:
- \(T\)与\(S\)有交的情况一定可以转化成\(T\)被\(S\)包含的情况
- \(T\)与\(S\)交集为空的时候,可以找一个连接\(S\)和\(T\)的集合\(M\)并操作\(S\cup T\cup M\),并将之前的所有操作连接到更外的层以及外层的连接部分同时操作,特殊处理最外层和第二层的情况
- \(T\)被\(S\)包含的时候,\(T\)落在某个完整区域内时等价于情况\(2\),否则一定连接若干个同色块,这些块可以同时处理并且显然答案不会更劣
那么我们可以枚举最后被修改的区域,这时答案就是将同色边边权当成\(0\),异色边边权为\(1\),而后举例这个点最远的黑色点的距离,对所有点取最小值即可
#include <bits/stdc++.h>
using std::pair;
using std::vector;
using std::string;
typedef long long ll;
typedef pair<int, int> pii;
#define fst first
#define snd second
#define pb(a) push_back(a)
#define mp(a, b) std::make_pair(a, b)
#define debug(...) fprintf(stderr, __VA_ARGS__)
template <typename T> bool chkmax(T& a, T b)
{ return a < b ? a = b, 1 : 0; }
template <typename T> bool chkmin(T& a, T b)
{ return a > b ? a = b, 1 : 0; }
const int oo = 0x3f3f3f3f;
string procStatus() {
std::ifstream t("/proc/self/status");
return string(std::istreambuf_iterator<char>(t),std::istreambuf_iterator<char>());
}
template <typename T> T read(T& x) {
int f = 1; x = 0;
char ch = getchar();
for(;!isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
for(; isdigit(ch); ch = getchar()) x = x * 10 + ch - 48;
return x *= f;
}
const int N = 50;
int n, m;
char g[N + 5][N + 5];
int dis[N + 5][N + 5];
const int dx[] = { 1, 0, -1, 0 };
const int dy[] = { 0, 1, 0, -1 };
int bfs(int x, int y) {
std::deque<pii> q;
memset(dis, -1, sizeof dis);
dis[x][y] = 0;
q.push_back(mp(x, y));
int res = 0;
while(!q.empty()) {
int cx = q.front().fst, cy = q.front().snd;
if(g[cx][cy] == '1')
chkmax(res, dis[cx][cy]);
q.pop_front();
for(int i = 0; i < 4; ++i) {
int nx = cx + dx[i], ny = cy + dy[i];
if(nx >= 0 && nx < n && ny >= 0 && ny < m && dis[nx][ny] == -1) {
if(g[nx][ny] == g[cx][cy]) {
dis[nx][ny] = dis[cx][cy];
q.push_front(mp(nx, ny));
} else {
dis[nx][ny] = dis[cx][cy] + 1;
q.push_back(mp(nx, ny));
}
}
}
}
return res;
}
int main() {
//freopen("paint.in", "r", stdin);
//freopen("paint.out", "w", stdout);
read(n), read(m);
for(int i = 0; i < n; ++i) {
scanf("%s", g[i]);
}
int ans = oo;
for(int i = 0; i < n; ++i) {
for(int j = 0; j < m; ++j) {
chkmin(ans, bfs(i, j));
}
}
printf("%d\n", ans + 1);
return 0;
}
讲课(杂题选讲)
Rest In The Shades
平面上有一个点光源,以每秒一个单位的速度从\((x_0,y)\)沿直线走到\((x_1,y),y\lt 0\),\(x\)轴上面有\(n\)条线段会遮挡光线,\(x\)轴上方有\(q\)个点。问每个点能够被光线直射的总时间
\(n,q\le 10^5\)
解:
反过来理解,把\(x\)轴上方的\(q\)个点看做点光源,下面移动的是接收器
那么要求的就是对于每个点光源,其能照射到接收器的总时间
那么每个点光源向周围发射的光经过\(x\)轴上面的线段遮挡之后到下面就形成了两个三角形(点光源和光在\(x\)轴上照亮的区间、点光源和光在移动路径上照亮的区间)
因为移动路径和\(x\)轴是平行的
考虑根据相似,先求出每个点在\(x\)轴上照亮的区间,再根据比值就求出了在移动路径上的长度
另外由于可能会出现:接收器并没有完全经过照亮的区间就停止移动了或者接收器开始移动的位置之前的部分也能够被照亮
所以对这两个区间(包含起点和终点的)进行二分,找到不合法(被照亮但是接收器没有经过这里)的区间,减掉这部分
用二分的原因:精度差小
Nastya and King-Shamans
给出一个长度为\(n\)的序列\(\{a_i\}\),现在进行\(m\)次操作,每次操作会修改某些\(a_i\)的值,在每次操作完成之后需要判断是否存在一个位置\(i\)满足\(a_i=\sum_{j=1}^{i-1}a_j\)并求出任意一个\(i\)
\(n,m\le 10^5,0\le a_i\le 10^9\)
解:
如果是单点修改的话,考虑维护式子\(a_i=\sum_{j=1}^{i-1}a_j\),变成\(a_i-\sum_{j=1}^{i-1}a_j=0\),显然对于单点修改的情况下这个非常好维护,直接查询是不是\(0\)就行了
区间修改的话,首先考虑一个暴力,每次从\(i=1\)开始统计\(\sum_i a_i\)然后判断是否合法,显然\(sum_i\)是单调不降的,符合条件的\(a_i\)一定\(\ge sum_i\)每次用线段树找到大于等于当前前缀和的最左边的\(a_i\)并且判断是否合法
因为“如果一个新的\(a_i\)不合法,那么当前前缀和至少会达到上一次的两倍”,那么这样查找的复杂度就是\(O(\log\omega)\),\((\omega\)是前缀和的最大值\()\),那么总复杂度就是\(O(m\log n\log\omega)\)
Raining Season
有一棵\(n\)个点的树,每条边有两个参数\(a_i,b_i\),在某一时刻\(t\),一条边的长度为\(a_i\times t+b_i\)
问在不同时刻\(t=0,1,2\cdots,m-1\)这些时刻树的直径
\(n,m,a_i\le 10^5,b_i\le 10^9\)
解:
考虑已经得到若干条路径后如何计算答案, 将每条路径表示成数对\((a_i,b_i)\), 从小到大枚举 \(t\) , 直径的 \(a_i\) 一定单调不降, 并且变化过程可以用 斜率优化维护.
\(a_i\times t+b_i\ge a_j\times t+b_j\rightarrow (a_i-a_j)\times t\ge b_j-b_i\)
\(-t\le \frac{b_i-b_j}{a_i-a_j}\)
发现是一个类似上凸壳的转移
但是这样的复杂度是\(O(n^2)\),复杂度依然是错的
显然我们需要的复杂度是\(O(n\sqrt n)\)或者\(O(n\log n)\)
问题变成如何减小路径数量, 比较容易想到的方法是树分治
由于跨越中心的两部分需要合并, 我们采用边分治
跨越中心的部分就是两个部分的点的闵科夫斯基和.
$Minkowski Sum $
定义: 给出两个点集\(A,B\), 求点集$ C ={a+b | a\in A,b\in B}$的凸包.
解法: \(C\)中凸包上的点由\(A,B\)凸包上的点相加得到, 先求出\(A,B\)的凸包. 找到\(A,B\)一组相加后在凸包上的点, 向后按照相邻向量的极角顺序合并.
Make Symmetrical
给一个初始为空的整点集, 现在有\(n\)次操作:
- 1 x y向点集中插入\((x,y)\)
- 2 x y从点集中删除\((x,y)\)
- 3 x y询问至少需要添加多少个点满足这个点集形成的图像关于\((0,0),(x,y)\)的连线对称
\(n\le 2\times 10^5,0\le x,y\le 10^5\)
解:
注意到互相对称的两点到达原点的距离相同,即互相对称的两点在同一个圆的圆周上
因为这些点对都是整数,而圆上整数点的规模可以看做\(\sqrt n\)(实际远小于\(\sqrt n\),详细可见《隐藏在素数规律中的\(\pi\)》)
对于每个圆,维护其边界上点的集合,每加入或者删除一个点就计算 这个点和其它点的对称中心并统计答案
复杂度可以认为是\(O(n\sqrt n\log n)\)但是难以达到理论上界
Number Clicker
给出三个数\(u,v,p\),可以对\(u\)进行如下操作
- \(u\leftarrow u-1\mod p\)
- \(u\leftarrow u+1\mod p\)
- \(u\leftarrow u^{p-2}\mod p\)
求一种不超过\(200\)步的方案使得\(u\)最终变成\(v\)
\(0\le u,v\lt p\le 10^9\),\(p\)是质数
解:
操作\(3\)也就是费马小定理求逆元
因为操作\(3\)的存在, 我们可以近似地认为这张图是随机的, 但是直接搜的期望复杂度还是很大
考虑双向\(BFS\),先从\(u\)出发进行\(BFS\)并取出前\(\sqrt p\)个状态的值,显然这部分路径的长度不会超过\(100\)
接下来从\(v\)出发进行\(BFS\)并且在之前的答案中查询,根据生日悖论,\(\sqrt p\)次找不到解的概率接近\(0\)
生日悖论
一个班级,\(30\)个人中,有两人生日相同的概率接近\(100%\)
证明:
考虑所有人中没有人的生日相同的概率
第一个人与第二个人生日不同的概率为\(\frac{364}{365}\),第三个人不与前两人生日相同的概率为\(\frac{363}{365}\),第四人\(\cdots\),最后当人数为\(30\)的时候,可得三十个人生日两两不同的概率=\(1\times \frac{364}{365}\times \frac{363}{365}\times \cdots\frac{335}{365}\)
这个值经过收敛,非常接近\(0\)
因为"所有人中没有人生日相同"是"有至少两人生日相同"的对立事件
所以“至少两人生日相同“这一事件的概率是接近\(100%\)的
Distinctification
给出一个长度为\(n\)的序列\(x\),每个位置有\(a_i,b_i\)两个参数,\(b_i\)互不相同,可以进行任意次如下操作:
- 若存在\(j\ne i\)满足\(a_j=a_i\),则可以花费\(b_i\)的代价让\(a_i+1\)
- 若存在\(j\)满足\(a_j+1=a_i\),则可以花费\(-b_i\)的代价让\(a_i-1\)
定义一个序列的权值为将序列中所有\(a_i\)变得互不相同所需的最小代价。
问给定序列的每一个前缀的权值
\(n,a_i\le 2\times 10^5,1\le b_i\le n\)
解:
先考虑所有\(a_i\)互不相同的时候怎么做,若存在\(a_i+1=a_j\),则可以花费\(b_i-b_j\)的代价交换两个\(a_i\),显然最优方案会将序列中所有\(a_i\)连续的自断操作成按\(b_i\)降序的
如果有\(a_i\)相同,则可以先将所有\(a_i\)编程互不相同的再进行排序,但是这时候可能会扩大值域使得原本不连续的两个区间合并到一起,于是我们需要维护一个支持合并的数据结构
尝试用并查集维护每个值域连续的块,并且在每个并查集的根上维护一个以\(b\)为关键字的值域线段树,每次合并两个联通块的时候,合并他们对应的线段树即可维护答案
Scissoros
有两个串\(S,T\)和一个数字\(k\),可以将\(S\)中任意两个长度为\(k\)的不相交子串取出,并且按照顺序拼接到一起,求一种取串的方案使得\(T\)是这个新串的子串
\(|T|\le 2k\le |S|\le 5\times 10^5\)
解:
\(T\)一定存在某个分段点使得这个点的左边部分和右边部分分别是两个不相交子串的一个后缀和前缀,考虑枚举这个分段点,那么显然只需要满足左边部分第一次出现的标号小鱼右边部分最后一次出现的标号即可
进一步观察发现,对应\(T\)的所有前缀,第一次出现的位置是单调的,我们只需要做一次\(KMP\)即可得到美国前缀出现的最早的位置