8.13 模拟赛记录
本日记录含大量总结。
复盘
开局遍历,T1分治显然,T2二分答案但不好做,50分小根堆暴力显然,T3倍增但没啥别的思路,T4只能整20分暴力。
首先我认为最没发展前景的就是T4,花15分钟推完规律写上,直接关掉cpp。
然后就是T1,很显然是分治,每次把寻找的范围缩小4倍,搜到2x2之后考虑一下这个图形的形状(有4种),根据形状输出结果。说着轻松,但是实现过程确实比我想象的更困难,花了相当大功夫去想,终于是找到了规律,抱着切题的希望把这题认认真真做了。整完基本就大约10:10了。
T2认为二分答案实在不易实现,觉得T3拿高分更有希望,先直接写个小根堆暴力模拟,看T3。
到T3还有一小时,看到环,需要统计环上点数,SPFA和纯dfs显然都不好整,于是本能反应写缩点,写到一半发现缩点没用,于是删了重新写了一个dfs。写dfs就是奔着无环的情况去的,从1开始跑图,跑到n记一个答案,最后处理答案。为了多得一些分,做了一些特判,比如自环输出1,再有环就输出个2,这是考虑到已经特判过1了。
最后还剩10分钟,从容处理完文件,检查一下多测清掉没有,freopen写没写错,交上去。
预计分数:100+50+20+20.
复盘分析
得分:0+0+40+10.
T3显然骗分骗到了20分,很成功,没啥可说的。T4推测是取模取错了。
T1一开始发现一个智障错误,是先算的距离后乘10,这显然不对。改完之后发现还是爆零,这才发现是策略出了问题。经过一番处理,翻了一顿自己做题的草算纸,发现一种简单很多的做法,结合题解的提示才改出来。只能说爆零也不是特别冤。
T2爆零原因不明。
T3经过翻YBT题解,发现用Floyd实现很容易,因为这样不用纠结于环的问题。瞬间得到启发。
总归问题和昨天一样,思路和实现上有一些问题。我并不强求自己像别的dalao那样能快速地抓住关键,我抓不住也无所谓,毕竟考场上当时想不到就想不到,没啥可纠结的。但是得保证自己有一些策略,经过这几天的总结,有一些东西自己距离想出正解就差一步,这种差距我认为是对于考点应用的不熟悉导致的,不知道怎么用,尤其是不知道什么时候用的问题。
到CSP的这段时间,在巩固知识点的过程中,总归要总结一些联系上的经验。所以已经在改了,最近几天再改题的时候,不明白就问别人怎么做的,问思路,代码控制自己写,不要看了std之后去写,锻炼一下自己的实现能力和思路。总之我觉得我开窍的时候脑子也不是很笨,但是学OI到今天CSP及以上的考试,除了极个别白给的题,就没想出过正解。这其中大部分是需要进行一些知识点的缝合,说白了就是考你能不能想得到,想得到就是分数,想不到就没分了,比如一些dp,一些图论,其实知识点真不多,但是用的妙却很难(DP实属一生之敌,至今写的都不咋好,555),这不可能靠灵感,我觉得更多地还是在看到题的时候,能提出有意义的问题,然后对于子问题有n个对应的可能的解法,选一个最好的,然后拆下一个子问题。我觉得不论别的,一是必须熟练,要用的时候得能用,像前几天那样二维树状数组没等实现先调半个点就根本不合格;二是知道什么时候用,能跟别的分得开,多总结经验。CSP之前我也不求自己能多学会多少东西,就把这两点我最缺的做好,就行了。
另外,我希望开学以后能多睡会儿(醒醒吧,7点上学,上半夜不睡睡的比现在还少),集训完事以后我也尽可能调整一下状态。期末的时候一直靠自己摸索的疯狂喝水维持状态高水平到下午一点之后靠生物钟正常运作,愣是坚持住近一个月每天熬到1:30,干完作业做一些复习还能每一天花两个小时最终学完选修一(自学,说白了就是做了一本教材全解,180多页),现在水喝得少了,水是生命之源,得多喝 有机会调整一下,应该是有一些休息。然而实际并不是这样,我上一次上半夜睡觉应该是上周六(11:30以后了也是),再上一次是期末考试,再上一次就是六月了(捂脸)。一方面是提高一点效率,我认为主要是有的时候debug花的时间太长,读std的时间更长,有时候空想是浪费时间,有时候问一问别人会的更快还不用去看代码(不过有些时候没人回答就只能自己上了)。
我的身体系统也是挺神奇的,只要保持一个作息几天生物钟就会很容易调整,保证我上床之前都是是清醒的,但是作息经常变化脑子反应不过来就会连着很多天头疼(大概是大脑的调节系统都晕了)。所以长时间熬夜对于我,只要不再熬夜的时候有足够的的休息,而不是像期末那样就周六周日突然多睡个一两小时,加上喝水调节,其实不成问题。然而这几天我长期喝水变少,身体的运行速度变的越来越慢,我感觉自己已经开始叠“死亡buff”了,再不调整五天后大脑当场RE(捂脸)
现在觉得自己学的东西确实越来越多,但是有时候就是觉得心里没底,真的觉得学的挺透用的挺有感觉的越来越少了,确实变难了,单位时间信息量剧增,也是我确实没给自己培养好熟练度。总归有机会去改,希望这个假期正式集训的最后五天能实践自己的策略,考试好好考,考多少分倒是其次的,考多少名那完全没用,主要是减少看题就脑子一片空白和爆零的事情。以前是觉得自己太保守,现在有点浪不起来了。总之一步步来吧,甭管能力是不是真的在下滑,我都承认现在这个水平,剩下的问题也只是如何提高罢了。虽然排名不重要,但是希望起码回到以前前5-8的水平吧…
(部分)题解
这阶段集训过半了,稍微多总结了一点。接着回来看题。
除了T4由于某些原因,我开始做的时候看不懂题解的思路,而一众大佬都睡去了(这已经是我这个假期不知道第几次陷入“孤立无援”的尴尬处境了,笑),大部分能理解,但是有些关键没明白,听课的时候也没明白,所以等明天完全搞懂了再说。
T1分治,在考场上经过反复的研究n=3各种区域的出入边,得到了一堆复杂的结论,画了三页的图和各种推导,然而事后发现最有用的其实是这个在纸上并不起眼的图:
这个图真的可以说是非常形象了,果然,大道至简啊(捂脸)
很显然‘A’是更小一维的基本图形,这四个A是保证了出入性质都是完全对应的,不过就这么看,由于A是对称的,不便叙述,故再模仿这个画一个图:
很明显,B的头进尾出。这样一来不难发现,如果设‘B’为基本型,左上的就是沿y=x翻折得到的,左下的就是沿y=-x+2len(len为一个B的边长)翻折得到的。这一来,我们可以一直递归的确定每个点在所对应的区域中的序号,最后在回溯的过程中将其转化为真实的坐标。
这部分代码如下:
void find(int n,int id,int &x,int &y){
if(!n){
x = 1,y = 1;
return;
}
int temp = 1 << (n - 1),t = temp * temp,p = (id - 1) / t + 1;
find(n - 1,id - (p - 1) * t,x,y);
if(p == 1) swap(x,y);
if(p == 2) x += temp;
if(p == 3) y += temp,x += temp;
if(p == 4){
swap(x,y);
x = temp + 1 - x,y = 2 * temp + 1 - y;
}
}//翻折如果不好分析,很建议举例推导
T2是一个比较有技术含量的二分。
很显然,答案没法直接二分,故应该考虑一下二分得到某一种取的个数,然后快速幂做出结果。
考虑第k个物品取了t次,能与之争夺最大的也只有其他物品取t+1次了,1和k的差距只有k,在取的次数上,取1logk次就会等于k,故t不需要考虑很多,枚举logk次足够了,这可以在n/k的左右平衡一下,按照题解的说法,这可以是常数次(如下面的代码就是进行了10次)。
在二分的时候,首先算一下买了t个k之后还能买多少别的,记为left。之后先算一下要想买k(也就是使k的价值是目前能买的里面最小的)得在此之前买多少东西,如果<left,说明买到t个k之后能买更大的,直接continue。不然的话就在[1,k]之间二分,求一下买mid之前得买多少东西,如果<left,说明这东西不是买到最贵的,可以考虑更贵的;不然就考虑买更便宜的,最后就会得到买到最后哪一个物品最贵。结果就是l*2(因为是买了t+1个,为了保持一致,二分里算的是买t个的情况)和k(首先保证买了k,也即t不等于0)里的最大值乘以2^相对差值,还是先保持下界一致,最后再加上下界做快速幂。我认为这样做主要是为了让res尽可能小,到最后统计过最小值再去取模,不然统计出的最小值是有问题的。
一定要注意,千万别把买的个数和买的花费给混为一谈,虽然把这俩混一起听起来是非常滑稽的错误,但至少我写的时候总是把个数串台到花费,然后各种卡住,比如上面那个2 * l就让我卡了挺久。以及这题的std在函数里面为了适应自己的书写习惯(你得承认,在[1,k]里取n个数确实挺别扭),把n和k对调了,导致包括我在内的很多人看不懂std硬看了很久。所以,思路才是关键,硬看std很容易忽略一些思路上的关键和细节。
完整代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define int long long
const int mod = 1e9 + 7;
int n,k;
int ksm(int a,int b){
int ret = 1;
if(b == -1) return (1e9 + 8) / 2;
while(b){
if(b & 1) ret = ret * a % mod;
a = a * a % mod;
b >>= 1;
}
return ret;
}
signed main(){
int i,j,left,t,p,res,l,r,mid,s;
scanf("%lld",&t);
while(t--){
res = 1e18;
scanf("%lld %lld",&n,&k);
p = max((long long)0,n / k - 5);
for(i = p;i <= p + 10;i++){
l = 1,r = k,left = max((long long)0,n - k * i),s = 0;
for(j = r;j;j /= 2) s += j;//计算买r之前要买几个东西
if(s < left) continue;
while(l < r){
mid = (l + r) >> 1,s = 0;
for(j = mid;j;j /= 2) s += j;
if(s < left) l = mid + 1;
else r = mid;
}
res = min(res,max(2 * l,k * bool(i)) << (i - p));
}
res = res % mod * ksm(2,p - 1) % mod;
printf("%lld\n",res);
}
return 0;
}
T3其实想到Floyd之后就不是很难了,我们思考怎么维护time[i][j](即i到j花的最少时间)。这题的特别之处在于time与边权无关,而与由几个不同的2^k组成,所以从2 ^k的角度出发,记一下哪些点可以经过2 ^k步到达,这样的两点间time显然是1,我们就有了想要的“边权”。
实际写起来并不难写,注意一下Floyd循环顺序即可。
完整代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
bool dp[101][101][32];
int ecnt = -1,head[101];
long long time[101][101];
int main(){
int t,n,m,i,j,k,l,x,y;
scanf("%d",&t);
while(t--){
memset(dp,0,sizeof(dp));
memset(time,0x3f,sizeof(time));
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++) time[i][i] = 0;
for(i = 1;i <= m;i++){
scanf("%d %d",&x,&y);
dp[x][y][0] = 1;
time[x][y] = 1;
}
if(n == 1){
printf("0\n");//特判
continue;
}
for(l = 1;l <= 30;l++){
for(k = 1;k <= n;k++){
for(i = 1;i <= n;i++){
for(j = 1;j <= n;j++){
if(dp[i][k][l - 1] && dp[k][j][l - 1]){
//i-k与k-j都能走2^ l-1步通过,那显然i-j经过这条路径就能走2 ^l步通过
dp[i][j][l] = 1;
time[i][j] = 1;
}
}
}
}
}
for(k = 1;k <= n;k++){
for(i = 1;i <= n;i++){
for(j = 1;j <= n;j++){
time[i][j] = min(time[i][j],time[i][k] + time[k][j]);
}
}
}
printf("%lld\n",time[1][n]);
}
return 0;
}
/*
1
4 4
1 1
1 2
2 3
3 4
*/
T4矩阵快速幂是真的复杂,我只能以我自己的理解尝试解释一下。
考虑先竖着处理同样高的部分,然后横着处理不同高的部分。
这题首先得应用状压,最终答案就是所有同样高部分的方案累乘,这里也要进行状态的转移,大概是ans[i][S]=(sigma)ans[i-1][T]*dp[T][S],这个形态就已经是矩阵乘了,所以上矩阵乘法。
先研究状态,记A[i]表示高为i的一列的状态矩阵,为了获得这个状态,需要先状压求一下每种左右下边的覆盖状态一共多少方案,然后放进状态矩阵。如果左右都封住了,那么横边就不能封住了,记被封上为0,dp[i][j]表示推到第i行,下边是否被封的方案数,则状压如下:
dp[0][0] = 0,dp[0][1] = 1;
//......
if((j | k) & (1 << (l - 1))){//左、右、横边任意一个不被封
dp[l][0] = dp[l - 1][0] + dp[l - 1][1];
dp[l][1] = dp[l - 1][0] + dp[l - 1][1];
}
else{
dp[l][0] = dp[l - 1][0] + dp[l - 1][1];
dp[l][1] = dp[l - 1][0];//l-1的下边被封就是l的上边被封,不能从上边被封推过来
}
处理完了每一种高度一整列的情况,对于不管是同样高还是不同高的几列,我们都可以用状态矩形求幂,因为最终的方案数统计是乘法原理,而状态矩阵存的是状态对应的合法方案数,所以乘上所有的状态矩形的幂可以得到结果。最终的结果就是(0,0)位置的方案数。
这题虽然说的轻松,但我理解的过程极其之长,才能勉强把代码写出来,总之对于我是一道难题。
完整代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define int long long
const int mod = 1e9 + 7;
int width[10],dp[201][201];
struct Matrix{
int x,y,a[201][201];
}A[8],B;
Matrix operator * (const Matrix &l,const Matrix &r){
Matrix ret;
ret.x = l.x,ret.y = r.y;
int i,j,k;
for(i = 0;i <= ret.x;i++){
for(j = 0;j <= ret.y;j++){
ret.a[i][j] = 0;//不能省略
}
}
for(i = 0;i <= ret.x;i++){
for(j = 0;j <= ret.y;j++){
for(k = 0;k <= l.y;k++){
ret.a[i][j] = (ret.a[i][j] + (l.a[i][k] * r.a[k][j] % mod)) % mod;
}
}
}
return ret;
}
void Getdp(){
int i,j,k,l;
for(i = 1;i <= 7;i++){
A[i].x = A[i].y = (1 << 7) - 1;
for(j = 0;j < (1 << i);j++){
for(k = 0;k < (1 << i);k++){
dp[0][0] = 0,dp[0][1] = 1;
for(l = 1;l <= i - 1;l++){
if((j | k) & (1 << (l - 1))){
dp[l][0] = dp[l - 1][0] + dp[l - 1][1];
dp[l][1] = dp[l - 1][0] + dp[l - 1][1];
}
else{
dp[l][0] = dp[l - 1][0] + dp[l - 1][1];
dp[l][1] = dp[l - 1][0];
}
dp[l][0] %= mod,dp[l][1] %= mod;
}
if((j | k) & (1 << (i - 1))) A[i].a[j][k] = dp[i - 1][0] + dp[i - 1][1];
else A[i].a[j][k] = dp[i - 1][0];//把每种状态对应方案数放进矩阵
}
}
}
}
signed main(){
int i,j,k,l;
for(i = 1;i <= 7;i++) scanf("%lld",&width[i]);
Getdp();
B.x = 0,B.y = (1 << 7) - 1;
B.a[0][0] = 1;
for(i = 1;i <= B.y;i++) B.a[0][i] = 0;
for(i = 1;i <= 7;i++){
while(width[i]){
if(width[i] & 1) B = B * A[i];
A[i] = A[i] * A[i];
width[i] >>= 1;
}
}
printf("%lld\n",B.a[0][0]);
return 0;
}
温馨提示:不建议把^重载成快速幂,不然会面临重载=等一系列越解决越多问题,这也是为什么我在矩阵快速幂弃坑到现在的缘故(捂脸)
Thank you for reading!