csp冲刺考试记录总结
CSP2019冲刺考试记录总结
10.10(lost 100)
得分55分
T1期望100 \(\rightarrow\) 10
T2期望100 \(\rightarrow\) 35实际算法50分
T3 10分暴力
T1是一道分情况讨论的题 当时写了170+行,还只有10分
可以把有两个-1的情况用两个inf和一个\(3^k\)解决
然后画出没有-1,一个-1的情况,可以找到6种无解的情况,4种乘2的情况,剩下的乘1
- and得1与or得0冲突
- and得1与xor得1冲突
- or得0与xor得1冲突
- and = or = xor = 1冲突
- and=or=0与xor = 1冲突
T2算错复杂度
对于3000000(3e6)的数据,O(nlogn)是死的
考场上写了个set炸飞还挂了
用一个vector就解决了
学到一个访问负数下标的方法
bool a[N];
bool *vis = a+3500000;//用指针解决
T3写暴力还有10分的long long丢了
10.12 (lost 60)
得分100整,但是期望也不高
主要是只有第一打满了还花了大部分时间
最后一题以为是dp扎进去打了结果正解是贪心
暴力分至少有60分没拿,
T1是数学组合题就是重复元素的不重复排列数
很简单
我在考场上写的递推的也是正解,用两两合并的思想,就是把ci个相同的人一个个地插空,除去重复的全排列
T2是尺取法 也就是双指针
对于选择符合要求的另一属性,同时又要求区间的最大差最小差,就可用尺取法
通常先排序,然后维护保证双指针中间的内容满足条件,此时lr中间并不是真的全部选择了
int l=1,r=1;
while(r <= cnt)
{
buk[p[r].g]++;
if(buk[p[r].g] == c[p[r].g]) tot++;
if(tot == n)
{
ans = min(ans,p[r].h - p[l].h);
while(buk[p[l].g] > c[p[l].g])//填>是为了l++后保证维护了合法段 尺取法
{
ans = min(ans,p[r].h - p[l].h);
buk[p[l].g]--;
l++;
}
ans = min(ans,p[r].h - p[l].h);
}
r++;
}
10.20(215)
T1大模拟不过打了很久
T2是用背包进行最后统计
T3不会
还可以
10.21 (70)
以为这个题好难,但是下来有感觉挺简单的
T1经典的约瑟夫问题
首先可以搞出一个\(O(n)\)递推式\(f(n) = (f(n-1) + m) \% n\)
一定要根据打出来的表仔细观察,发现他是一个分块的等差序列表,我们发现在同一块中:\(i- f(i)\)是呈现出等差递减的,且\(d=m-1\),然后最后可以通过这个差值算出这一块有多少元素,可以飞快跳到这一块中某个元素或者这一块最后一个元素。而想要到下一块的话,就在这一块最后一个元素使用递推的方法即可
假如已知这一块的第一个是第\(i\)个,其值为\(f(i)\),则这一块的元素个数为\(\lfloor \frac{i-f(i)}{m-1} \rfloor = step\)
inline int f(int n,int m)//必须要自己照着打出来的表来跳 才能懂
{
int pos = 1,ans = 1;//这个答案表是分为等差数列的,可以用这个性质去跳
while(pos <= n)
{
int nxtpos = pos+1;
int nxtans = (ans + m) % nxtpos;
if(!nxtans) nxtans = nxtpos;//计算下一块的起点和它对应的函数值
int step = (nxtpos - nxtans) / (m-1);//由下标减去函数值可以得到这一块的长度
if(nxtpos + step >= n)//如果答案在这一块中
{
int ret = nxtans + m * (n - nxtpos);
return ret;
}
nxtpos += step;
nxtans += step * m;//跳到这一块的末尾
pos = nxtpos;
ans = nxtans;
}
return ans;
}
T2是数学和式转化+数据结构维护
一般分配律(非常重要!)
交换求和次序公式
这个题给出一堆向量,让你求区间向量对的叉乘的平方和
对第二项改变求和次序:
此时第二项的k与第一项的i等价,注意第一项的k和第二项的i可以合并,相当于两个三角形拼在一起形成正方形
i==k时叉乘为0,不碍事,接下来用一般分配律
漂亮!接下来我们对右边同样这么搞
噔噔咚!
用三棵bittree,分别维护:\(a_i^2\)的2前缀和,\(b_i^2\)的前缀和,\(a_ib_i\)的前缀和
T3经典模型:LCIS最长公共上升子序列
懒得再打一遍,详细见我的手写笔记本
10.22(AK300分!)
数据出错了害我不能AK
T1数据结构题,sol给了一个\(O(nlog^2n)\)的线段树做法,但是我用ODT水过去了,值得一提的是慢慢摸还hack到自己然后de了一会bug,原题来自UESTC秋实大哥与战争
T2原题序列合并,十几分钟就A了,还打了个对拍
T3二分答案+贪心,双倍经验:序列变换
T3是个经典模型:求最小的最大把序列调成严格递增的费用
二分一个费用,把前面的都压到尽量小,然后如果这里上升了mid都不合法,那么return false
10.23(80)
今天是真的涉及我的知识盲区
事实证明:想象力是算法设计的重要一环
绝了今天他们的错贪心可以AT1而且subtask没卡掉他们,suke随便摸了一组hack,椿雄就过不了
T1是一个最大子段和线段树
模型是最大M段和
考场上打了一个\(O(n^3)\)dp,是有正确性的,但是艹他就是没给三方的分
更绝的是,直接把所有正数加起来就可以过?虽然没人这么骗分,但是垃圾数据恶心我
关于这个std的正确性我们可以手动模拟一下
15 4
8 -2 6 8 -4 -3 -6 8 14 10 0 -8 -4 10 11取1,15
-8 2 -6 -8 4 3 6 -8 -14 -10 0 8 4 -10 -11
取5,7
-8 2 -6 -8 -4 -3 -6 -8 -14 -10 0 8 4 -10 -11
取11,13
-8 2 -6 -8 -4 -3 -6 -8 -14 -10 0 -8 -4 -10 -11
取2,2
-8 -2 -6 -8 -4 -3 -6 -8 -14 -10 0 -8 -4 -10 -11
可以看到,到最后一次取的时候,所有的正数都变成了负数,不再存在正的答案。这意味着通过加上被取反的原负值,一段答案被分成两段更优的答案,满足了最大M段和
跟我以前打过的不同,这棵线段树显然还需要支持求最大子段和的l和r值
打了一遍std感觉学到了很多
inline node operator + (const node a,const node b){
node ret;
if(a.lmax > a.sum + b.lmax) ret.lmax = a.lmax , ret.lmax_rpos = a.lmax_rpos;
else ret.lmax = a.sum + b.lmax , ret.lmax_rpos = b.lmax_rpos;
if(b.rmax > a.rmax + b.sum) ret.rmax = b.rmax , ret.rmax_lpos = b.rmax_lpos;
else ret.rmax = a.rmax + b.sum , ret.rmax_lpos = a.rmax_lpos;
ret.sum = a.sum + b.sum;
if(a.smax > b.smax) ret.smax = a.smax , ret.ansl = a.ansl , ret.ansr = a.ansr;
else ret.smax = b.smax , ret.ansl = b.ansl , ret.ansr = b.ansr;
if(a.rmax + b.lmax > ret.smax) ret.smax = a.rmax + b.lmax , ret.ansl = a.rmax_lpos , ret.ansr = b.lmax_rpos;
return ret;
}
首先就是上面那个重载+法,泛用性很好。
保存ansl,ansr的技术:通过维护这个区间的 lmax的lmax_rpos和rmax的rmax_lpos,在维护这个区间的答案smax时,更新对应的ansl和ansr,因为存在一种情况就是答案=a.rmax+b.lmax,所以需要上面两个值
区间取负:无论怎样,一个数的取值只有正负两种,所以我们直接建两棵线段树,然后交换两者信息打上标记
这个标记稍微有点东西
tag[rt]表示这个区间reverse了多少次,1为奇数次,0为偶数次相当于没有reverse
T2才是最水的啊!!!
把一个序列往双端队列里面放,可以放头放尾,求之后的LIS
由于LIS不存在两个值相同的,所以我们直接把他reverse一下接在前面,,再直接求LIS即可
\(O(nlogn)\)求LIS有两种:用lower_bound、或者离散化+树状数组
for(int i=1;i<=n+n;i++)
{
if(arr[i] > low[top]) low[++top] = arr[i];
else *lower_bound(low+1,low+1+top,arr[i]) = arr[i];//lower维护贪心
}
T3是神秘的打表
后面的三角好找,然后有的行和列,对角线也好找,就是前面的三角需要有另一个东西地推过来
总之找规律出奇迹
1 | 1 | 1 | 1 | 1 |
---|---|---|---|---|
4 | 5 | 6 | 7 | 8 |
9 | 16 | 22 | 29 | 37 |
16 | 37 | 64 | 93 | 130 |
25 | 71 | 149 | 256 | 386 |
自己看
10.24(20垫底)
jiangly出的题,毒瘤要我命
T1 没想过\(-0.XXX\)+读入乱七八糟 \(100 \rightarrow 0\)
T2打了个暴力就走了20分,下来发现超级简单就是裸的topsort
T3期望dp完全不会
T1是循环小数化分数
对于一个有理小数例如\(0.857[142857]\)
它由整数,有限小数部分,无线循环部分组成
\(0.857 = \frac{857}{1000}\) \(0.000[142857] = \frac{142857}{999999000}\)
几个部分相加即可,读入一定全都用string转整数,不然会被-0搞死
T2就是topsort 求最长路,topsort自带判环
把字符转化成长度为1的边,跑26次最长路
T3化和式+数据结构维护
10.25(120 再次倒数)
这套题主要还是我太过粗心
T1打满了主要是自己谨慎地验证了结论,还打了暴力对拍,可是可以hack的都是小数据,以后这种结论题一定要验证小数据
T2是一个区间dp,但是有一个最后阶段的小问题没想完就在打,打完了打错了还觉得毫无违和感,而且过了样例卧槽
最后30分钟手动一模就hack了,然后就是痛苦地补救,直到交了之后才发现
区间合并时枚举了分成几段,然后又枚举了断点,枚举断点的作用并不是把左右k-1段的区间合并,很显然是把左边区间给乘上一段新的,然后利用了前缀和,保证了\(O(n^4)\)
最后发现自己前面读入之后没取模,后面倒是模得起劲
for(int len=2;len<=n;len++)//做区间dp
{
for(int i=1;i<=n+n;i++)
{
int j = i+len-1;
if(j > n+n) continue;
for(int k=2;k<=min(len,m);k++)//枚举分成几部分
{
g[i][j][k] = 0x7f7f7f7f;
for(int d=i;d<j;d++)//枚举断点
{
if(k-1<=d-i+1) f[i][j][k] = max(f[i][j][k] , f[i][d][k-1] * f[d+1][j][1]);
if(k-1<=d-i+1) g[i][j][k] = min(g[i][j][k] , g[i][d][k-1] * g[d+1][j][1]);
}
}
}
}
T3是一个树形dp,但是我状态设计错了,觉得空间够就设计了无用的状态,然后被自己弄混了
\(f[x][i]\)表示x点在载重为i的时候的最大亮度,不包含自己
当然这个转移就是\(O(n^2)\)的合起来仍然是题目所要求的\(O(n^3)\)
保存路径也不难,在答案更新时把路径复制过来就可以了,枚举时就是根和子节点分配的载重,然后很好写了
int f[402][202];//表示x利用了j的载重力,不包括自己
vector <int> g[402][202],path;//利用vector保存路径
inline void dfs(int x)
{
for(int d=0;d<(int)G[x].size();d++)
{
int v = G[x][d];
dfs(v);
register int i=0,j=0;
for(i=p[x];i>=0;i--)//分配根的载重
{
for(j=0;i-j-w[v]>=0;j++)//分配下面v点的载重
{
if(f[x][i] <= f[v][j] + l[v] + f[x][i-j-w[v]])//最后被忘了算上v的重量
{
f[x][i] = f[v][j] + l[v] + f[x][i-j-w[v]];
g[x][i] = g[x][i-j-w[v]];//直接复制路径,注意这里是两条路径的合并
// g[x][i].insert(g[x][i].end(),g[v][j].begin(),g[v][j].end());这么写稍微慢些
for(int k=0;k<(int)g[v][j].size();k++) g[x][i].push_back(g[v][j][k]);
g[x][i].push_back(v);
}
}
}
}
}
总结一下,如果数据小一定要手摸hack自己,不管可不可以验证,万一爆了呢?
一定要大胆猜想,小心求证,也要敢于提出质疑和推翻自己,走上正确的思路后自然算法水到渠成,可是栽在这里就太过可惜
dp虽然在复杂情况下力求状态泛化表示,可是如果要求的东西太多,不如专心让他只表示答案的一部分,实在不行还有g数组呢
10.26(20分)
没想到是雅礼的题,我说这两天怎么眼皮跳呢,就跳出来个这
昨晚上做梦梦见考试时3道题难度是倒着的,结果今天T1爆难,人人都在打搜索但是我交了个dp
T1正解是一个非常厉害的的状压dp
给定一个 \(n\times m\) 的按钮矩阵,按下每个按钮可以使自己和上下左右的状态变为 1,但要花费 \(a_{i,j}\)的代价。每个按钮有一个初始 0/1 状态,问将 \(n\times m\) 的矩阵状态全部变为 1 的最小代价。
在考场上写了一个涉及两行的dp,枚举它上一行的覆盖状态和这一行的覆盖状态,一算复杂度是够的,然后 就挂了。显然单单枚举这两个东西是无法保证正确性的,因为上一行想要被覆盖就必然要考虑上上行。这样一种可以涉及三行的策略选择类dp,显然要想办法枚举三行的状态
然后题解给出一个70分的dp,如果我多写几个小时说不定能想到,
令\(f[i][k][u]\)表示第i行的选择状态为u,第i-1行的选择状态为k时的保证i-1行及以前全部被覆盖满的最优解,不保证第i行是否覆盖完
观察一下这个设计,它虽然看似有后效性,但由于它的第i行不一定覆盖完,所以在枚举第i+1行时才能配合之前的状态得出合法转移
枚举第i行,第i-1行,第i-2行的选择状态,然后就可以得出第i-1行的覆盖状态,验证是否完全覆盖,然后转移即可
for(int i=2;i<=n+1;i++)
for(int j=0;j<=maxs;j++)//第i-2行
for(int k=0;k<=maxs;k++)//第i-1行
for(int u=0;u<=maxs;u++)//第i行
{
int sk = ((j|u|pic[i-1]|k)|((k<<1)|(k>>1)))&maxs;//第i-1的覆盖情况
if(sk==maxs)//保证上一行 填满
f[i][k][u] = min(f[i][k][u], f[i-1][j][k] + cost[i][u]);
}
注意最后的答案需要枚举\(f[n+1][s][0]\)这里是保证前n行都填满
然后是100分的优化
令\(f[i][j][k]\)表示第i行选择状态为k,覆盖状态为j时的最优解
显然k是j的子集,这样就删去了大量的无效状态,
这里记住一个小技巧:枚举子集
for(int k=j;;k=(k-1)&j)//枚举j的子集(第i-1行的选择状态)
{
//搞事情
if(k==0) break;//0也是子集中的一个,所以要上面执行完才退出
}
这下枚举上面i-1行的覆盖状态j和选择状态k,然后枚举第i行的选择状态,就可得到第i行的覆盖状态,注意到这里在已知所有上面和这一行的选择状态是,覆盖状态是通过计算唯一得到的,那么看似枚举j和k是无序的,然而判一下inf就可以得知是否是合法状态了
最妙的一点莫过于选择状态k是由覆盖状态j枚举子集得来的
第i-1行的覆盖状态j和选择状态k是 有机结合的,用他们两个就可以在满足i-1行填满,第i行位置的转移了
在统计答案时,也要仿造转移的方式,枚举覆盖状态后保证覆盖第n行,最后枚举选择状态即可
for(int i=1;i<=n;i++)
for(int s=0;s<=maxs;s++)
for(int j=1;j<=m;j++)
cost[i][s] += val[i][j] * ((s>>(m-j)) & 1);
for(int s=0;s<=maxs;s++)
{
int ss = (s|(s<<1)|(s>>1))&maxs;//左移右移就得到覆盖状态
f[1][ss][s] = cost[1][s];//置第一行的初始状态
}
for(int i=2;i<=n;i++)
for(int j=0;j<=maxs;j++)//枚举第i-1行通过k的选择 的覆盖状态
for(int k=j;;k=(k-1)&j)
//枚举j的子集(第i-1行的选择状态),保证了舍弃无用状态,利用了合法状态
{
if(f[i-1][j][k]<inf)//保证可以到达这个情况
{
for(int u=0;u<=maxs;u++)
{
int sup = (j|u|pic[i-1])&maxs;
int si = (u|(u<<1)|(u>>1)|k)&maxs;
if(sup==maxs)//保证第i-1行填满
f[i][si][u] = min(f[i][si][u], f[i-1][j][k] + cost[i][u]);
}
}
if(k==0) break;
}
for(int s=0;s<=maxs;s++)//枚举最后一行的覆盖状态
{
if((s|pic[n]) == maxs)
{
for(int u=s;;u=(u-1)&s)//枚举最后一行的选择状态
{
ans = min(ans,f[n][s][u]);
if(u==0) break;
}
}
}
io << ans;
10.28(270)
今天这套题非常的可做
T1打表找递推式,我先摸了前10的情况,猜对了一半然后才想通暴力怎么打,顺序错了
打了暴力发现错了从32往后错掉了,改了一下就过了,exciting
这个递推式:
for(int i=2;i<=n;i++)
{
if(i%2==0) f[i] = (f[i-1] + f[i>>1]) % mod;
else f[i] = f[i-1];
}
证明:
首先知道奇数的答案就是对应的偶数的答案,因为相当于偶数的答案+1
即\(f(n) = f(n-1) \ \ \ (n是奇数)\)
如果n为偶数,那么把他的方案分成两部分:包括1的和不包括1的
包括1的显然就是他的上一个数的所有方案每个+1,
不包括1的至少都包括2,所以显然是比它小一半的数每个方案都乘2
故\(f(n) = f(n-1) + f(\lfloor\frac{n}{2} \rfloor)\)
其实最好想的还是用完全背包做,物品就是2的方幂,草这么想真的太简单了
for(int b=1;b<=n;b<<=1)
for(int i=1;i<=n;i++)
f[i] = (f[i] + f[i-b]) % mod;
T2一开始觉得是求最长路,然后多画了几张图,发现其实是求最短路里面的最长值。
考虑两个点的高度差,他们至多只差最短路*d的高度,其余来的更长路,由于要满足最短路,不可能使答案更大
但是我偏偏没画图不连通的情况,于是-1的情况有一大堆没判,丢了30分
T3是有一个贪心的思想,但是一想的确只能这么做,
思考把一个序列放好的最小代价,肯定是要使移动的数最少,那么剩下没有移动的一定是连续上升的,要是剩下没有移动的最大,那么就是求最长连续上升子序列
这个东西用dp求,非常地简单
for(int i=1;i<=n;i++)
{
f[arr[i]] = f[arr[i]-1] + 1;
ans = max(ans,f[arr[i]]);
}
io << n - ans;
今天学到的:
不到最后一刻绝不放弃hack!
10.29 - 1(210)
T1是昨天T3绝了总之秒了100分
T2是一个类似模拟,写一个暴力把所有碰撞时间按顺序执行有70分
正解是一个链表+模拟+优先队列,每下一次碰撞一定发生在两个相邻的之间,用优先队列每次取出最靠前的一个碰撞event,用链表维护相邻关系
代码上有很多可学的地方
struct frac{
ll fz,fm;
frac(){}
frac(ll fz,ll fm) : fz(fz) , fm(fm){}
friend inline bool operator < (const frac a,const frac b){
return a.fz * b.fm < a.fm * b.fz;//比较分数的方法,交叉相乘
}
friend inline bool operator == (const frac a,const frac b){
return a.fz * b.fm == a.fm * b.fz;
}
inline frac fix(){ll d = gcd(fz,fm);fz /= d, fm /= d;return *this;}
inline void print(){fix();printf("%lld/%lld\n",fz,fm);}
};
struct event{
frac time;
int id;
event(){}
event(const frac &_time,const int &_id):
time(_time) , id(_id){}
friend bool operator < (const event a,const event b){
if(a.time == b.time) return a.id < b.id;
return a.time < b.time;
}
};
set <event> s;
sort(a+1,a+1+n,cmp);//按位置排序
for(int i=1;i<=n;i++) nxt[i] = i+1 , pre[i] = i-1;
nxt[n] = 1 , pre[1] = n;
for(int i=1;i<=n;i++) s.insert(event(gettime(i,nxt[i]) , i));
event nows;
for(int i=n-1;i>=1;i--)
{
nows = *s.begin();
if(i == 1) break;
int out = nows.id;
if(a[out].power > a[nxt[out]].power) out = nxt[out];
s.erase(event(gettime(out,nxt[out]) , out));
s.erase(event(gettime(pre[out],out) , pre[out]));
nxt[pre[out]] = nxt[out];
pre[nxt[out]] = pre[out];
s.insert(event(gettime(pre[out],nxt[out]) , pre[out]));
}
nows.time.print();
T3是一个背包,我打了40分暴力,正解暂时没懂 占坑
10.29 -2(210)
牛客\(CSP-S\)提高组赛前集训营1,是好题
\(T1\)仓鼠的石子游戏
是一个博弈论,首先先手必败,1的个数为奇数个,则胜负逆转
\(T2\)乃爱与城市拥挤程度
换根\(dp\),我现在只写出第一问 占坑
然后看到\(jiangly\)的代码他写的\(C++14\)给我的幼小心灵造成震撼
\(update\)
看了阿符+问了一下他,我好像懂了现在
求每个点 距离他为在\(k\)以内的点的个数,以及当以这个点为根时,这些\(k\)以内的点提出来来一颗子树,然后求每个点的\(siz\)的乘积。
换根\(dp\)就是要收得狂野,下放得谨慎
点的个数是好求的,设\(f[x][i]\)表示离点\(x\)距小于等于\(i\)的点的个数
下放的时候,减去外面的值即可
然后重点还是乘积
设\(g[x][i]\)表示不含根时的答案,\(G[x][i]\)表示含根时的答案
一口气放上来
inline void dfs(int x,int fa)
{
for(int i=0;i<=k;i++) g[x][i] = f[x][i] = 1;
Auto(i,x)
{
int v = e[i].v;
if(v==fa) continue;
dfs(v,x);
for(int j=1;j<=k;j++) f[x][j] += f[v][j-1] , g[x][j] = g[x][j] * G[v][j-1] % mod;
}
for(int i=0;i<=k;i++) G[x][i] = g[x][i] * f[x][i] % mod;//含根,手动乘上
}
inline void dfs_sp(int x,int fa)
{
if(x!=1)
{
for(int i=k;i>=2;i--) //f[fa][i-1] - f[x][i-2]表示换根时外面的与x距离为i的点
g[x][i] = g[x][i] * g[fa][i-1] %mod * ksm(G[x][i-2] , mod-2) % mod * (f[fa][i-1] - f[x][i-2]) %mod;
for(int i=k;i>=2;i--)
f[x][i] += f[fa][i-1] - f[x][i-2];
f[x][1]++;//手动算上父亲
for(int i=0;i<=k;i++) G[x][i] = g[x][i] * f[x][i] % mod;//依然是手动乘根
}
Auto(i,x)
{
int v = e[i].v;
if(v==fa) continue;
dfs_sp(v,x);
}
}
就是这一句
g[x][i] = g[x][i] * g[fa][i-1] %mod * ksm(G[x][i-2] , mod-2) % mod * (f[fa][i-1] - f[x][i-2]) %mod;
乘上父亲的不含根的\(i-1\)的答案,得到\(i\)的,把重复的除掉(乘逆元),此时\(fa\)到\(x\)的\(siz\)一定是变了的,而\(f[fa][i-1] - f[x][i-2]\)这个式子虽然在转移\(f\)时用到,但在这里就是\(fa\) 的新的唯一的贡献
换根法的特点
改变的dp信息其实非常少,往往只影响到x,y两个节点。
用这么一棵树来研究,有奇效
\(T3\)小w的魔术扑克
只打了暴力有30,可是把数据放过去居然可以达到70我傻了,
正解是并茶几
k张正反面的卡片,打出时只能选择一面,查询q次,每次问是否能组成l到r的顺子
10.30(40)
又炸了 ,依旧还是难一点的题拿不到分
今天T1暴力本来大家都打的60分,我打了30分就走了,心想60分跑不出来,没想到直接把第三层推出来,然后新加入的点一起循环起走就可以
正解是并查集,格点上的行列问题可用并查集处理
思考一个矩阵由两个行两个列组成,我们让四个点之间有联系,那么就是把这些行和列当做并查集的点连起来,一个点在被选中,当且仅当其余三点被选中,那么其余三点已经将行和列全部并在一起,而新的点只需要判断行和列是否在同一并查集即可,
为什么有传递性?观察发现,通过一个行连在一起的两个列上,只要有一个新的行插入,那么产生的在两个列上的交点,必然会属于一个矩形,同理两个行与新插入的列也一样。这个其实不叫传递性,是广义上的性质合并
一个技巧二维前缀和,这个题里面是静态矩阵查询,所以用前缀和\(O(1)\)解决
sum[x][y] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
sum[i][j] += sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1];
}
}
for (int i = 1, x1, y1, x2, y2; i <= q; i++) {
io >> x1 >> y1 >> x2 >> y2;
io << sum[x2][y2]-sum[x2][y1-1]-sum[x1-1][y2]+sum[x1-1][y1-1] << '\n';
}
T2是一个期望dp+kmp