这不是一份成绩单,而是一份「下次能多拿分」的攻略。看完它,你会发现:很多分丢得很可惜,但每一处都能学会。
复盘从表扬开始,这些都是真本事,要记住它们。
两次分数差的那 20 分,全部在「正方形」这道题上。这中间藏着一个很重要的道理,下面专门讲。
-O2 编译输出是空的,用 -O0 编译又输出 2。
数列 7, 77, 777, 7777 …… 第一次出现 K 的倍数是第几项?没有就输出 -1。(K 最大 100 万)
long long sum=0; for(long long i=1; i<=K; i++) { sum = sum*10+7; // 一直在累加真实的大数字 if(sum%K==0) { j=i; break; } } cout<<j;
long long sum=0; for(long long i=1; i<=K; i++) { sum = (sum*10+7) % K; // 每一步都取余数,数字永远很小 if(sum==0) { j=i; break; } } cout<<j;
你看,只改了一个地方:把 sum*10+7 变成 (sum*10+7)%K。就这一处,从 80 分变满分。
问题出在「数字会越长越大」。数列是 7、77、777……到第 19 项左右,这个数已经接近 7.7 × 1018,而 long long 能装的最大数大约是 9.2 × 1018。再往后,数字「装不下了,溢出」,就像水杯倒满还继续倒,溢出来的部分变成了垃圾值。
一旦溢出,sum 就成了乱码,sum % K 算出来也全是错的。于是答案比较大的那几个测试点(比如答案是 168 的第 11 个点、答案是 148 的第 13 个点)你都算不出来,错误地输出了 -1。
核心思想叫「边算边取模」:我们只关心「能不能被 K 整除」,也就是只关心余数。而余数永远在 0 到 K-1 之间(最多 100 万都不到),永远不会溢出。所以每一步都先 %K,既不影响判断,又彻底躲开了溢出。这是数论题里最最常用的一招,一定要记住。
给一个 n×n 的方格图,每格是 0 或 1,求「里面全是 0 的最大正方形」的边长。(n 最大 3000)
int ans,cur,xmin,ymin,xmax,ymax; // 这些是全局变量,会被反复污染! void dp(int x,int y){ if(vis[x][y]) return; vis[x][y]=1; xmax=max(xmax,max(xmin,x)); // 用"走过的点的包围盒"猜边长 ymax=max(ymax,max(ymin,y)); if(a[x+1][y]=='0' && a[x][y+1]=='0' && a[x+1][y+1]=='0'){ dp(x+1,y); dp(x,y+1); // 递归向右下扩展,n=3000 时栈会爆 } cur=min(xmax-xmin+1,ymax-ymin+1); }
// 来自标准答案 std/square.cpp —— 经典动态规划 int n,ans; char g[3003][3003]; int f[3003][3003]; // f[i][j]=以(i,j)为右下角的最大全0正方形边长 int main(){ cin>>n; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){ cin>>g[i][j]; if(g[i][j]=='1') f[i][j]=0; else f[i][j]=min(f[i-1][j],min(f[i-1][j-1],f[i][j-1]))+1; ans=max(ans,f[i][j]); } cout<<ans; }
min(上面、左边、左上 三个邻居的边长) + 1。
① 算法本身就不成立。你用 DFS 一路向右下走,再用「走过的点的最小/最大坐标」围成一个长方形(包围盒)来估算边长。但是——包围盒里不保证全是 0!而且 xmax / ymax 是全局变量,跨多次调用一直累加、从不清零,会互相污染。
反例:一个 4×4 的图,前两行全是 0,后两行右半边是 1。正确答案是 2(只能放 2×2),但你的程序会输出 4。
② 未定义行为(UB)。这是最危险的。同一份代码、同一个输入(测试点 02),用 -O2 编译输出「空」,用 -O0 编译输出「2」。结果会随机器和编译选项漂移——这就是「靠运气」的代码,今天对明天错。
③ 递归太深会爆栈。n 最大 3000,DFS 一路递归下去层数太多,电脑的「栈」放不下,直接崩溃(测试点 14–17 全部运行时错误 RE)。
那 20 分哪来的?测试点 01、03 你恰好输出 2,刚好等于答案,纯属巧合。这就是前面说的「争议 20 分」的真相。
小贴士:正解里用 cin>>g[i][j] 一个一个读字符也行,但 n=3000 时有 900 万个字符,更快的做法是用 scanf("%s", g[i]+1) 整行读入,避免超时。
手里有 m 元,n 个项目各要成本 c、能净赚 p,最多做 k 个;每次只能做「现在买得起的」。求最后有多少钱。(数值可达 10 亿,答案可达 1000 亿!)
int main(){ int n,k,m; // ① m 用 int!答案上千亿必溢出 cin>>n>>k>>m; t a[n+1]; // ④ 变长数组开在栈上,n=10万会爆栈(RE) for(int i=0;i<n;i++) cin>>a[i].c>>a[i].p; sort(a,a+1,cmp); // ② 致命笔误!只排了 a[0] 一个元素=没排序 for(int i=0;i<k;i++) for(int j=0;j<n;j++) // ③ 双重循环 10万×10万=百亿次,超时 { ... } }
// 来自第1名 马秀哲 ball.cpp —— 非常干净 #define int long long // 一句话让所有 int 都变 long long,告别溢出 pair<int,int> a[100001]; priority_queue<int> q; // 大根堆:自动把利润最大的浮到堆顶 signed main(){ cin>>n>>k>>m; for(int i=1;i<=n;i++) cin>>a[i].first>>a[i].second; sort(a+1,a+n+1); // 按成本从小到大排好 while(k--){ while(cc<=n && a[cc].first<=m){ // 把现在买得起的都放进堆 q.push(a[cc].second); cc++; } if(!q.empty()){ m+=q.top(); q.pop(); } // 取利润最大的去做 } cout<<m; }
① 用 int 装大数 答案能到 1000 亿,而 int 最多只能装约 21 亿。结果大数据点你输出了 1000000376,正确答案是 10750038174——数字太大装不下,溢出成乱码。钱、计数、答案这类可能很大的量,一律用 long long。
② sort(a,a+1,cmp) 这是最可惜的一处笔误。sort(a, a+1, ...) 表示「只排序从 a[0] 到 a[1] 之间」,也就是只有 1 个元素,等于完全没排序!应该是 sort(a, a+n, cmp)。因为没排序,你的贪心顺序全乱了(小数据你输出 4,正确是 14)。
③ O(n×k) 双重循环 你每做一个项目就把所有 n 个项目从头扫一遍。n 和 k 都是 10 万时,要算 100 亿次,电脑 1 秒只能算几亿次——必然超时。正解用「堆」把找最大利润的操作从「扫一遍」降到「直接取堆顶」,快了几千倍。
④ 变长数组 t a[n+1] 这种「长度由变量决定」的数组开在「栈」上,n=10 万时栈放不下,程序直接崩溃(段错误 RE)。应该在函数外面开成固定大小的全局数组 a[100005]。
正解思路(贪心 + 大根堆):先把项目按成本从小到大排序;用一个指针,每轮把「成本 ≤ 当前资金」的项目全丢进大根堆;然后取堆顶(利润最大的)去做,钱变多了,又能解锁更多项目……重复 k 次。这就是「滚雪球」:钱越滚越多。
N 名选手循环赛,每人按给定顺序依次比赛,每天每人最多一场,求最少需要几天,办不到则 -1。(N 最大 1000)
int main(){ cout<<-1; // 直接放弃,赌答案是 -1 return 0; }
你恰好「蒙」中了 10 个答案确实是 -1 的测试点,拿了 30 分。但其实……
把「每一场比赛」当成一个点。规则要求每个选手必须按自己给定的顺序打,所以对同一个选手来说,「他的上一场」必须排在「他的下一场」之前——这就在两场比赛之间连了一条「先后」的箭头。
这样会得到一张「有向图」。最少天数 = 这张图里最长的一条箭头链有多长(这条链上的比赛只能一天打一场,没法并行)。如果图里出现了「转圈」(A 要在 B 前、B 又要在 A 前),那就是死循环,输出 -1。
第 3 名张柏林用了一种很聪明的直接模拟写法:用 nxt[i] 记录选手 i 下一个要打谁,每一「天」让所有「双方都想打对方」的配对同时开打,打一对算一天,直到全部打完。没有人能打就说明卡住了,输出 -1。这题他拿了满分 100。
前三名的代码,差别不在「聪明」,而在「习惯」。
马秀哲在 number 和 ball 里都用了一招——开头写一句 #define int long long,让整份代码的 int 自动变成大整数,从此不用担心溢出。这正是你 ball 题丢分的根源。
// 马秀哲 number.cpp 开头 #define int long long signed main(){ ... }
和你 number 题正解一模一样的思想。马秀哲每一步都 %k,数字永远很小,绝不溢出。
// 马秀哲 number.cpp int l=7; for(int i=1;i<=1000001;i++){ if(l>=k && l%k==0){ cout<<i; return 0; } l%=k; l*=10; l+=7; // 先取模再继续算 }
square 三位高手都用了同一套经典动态规划(和标准答案一致);ball 用大根堆 priority_queue。他们不硬写慢办法,而是用对的算法。看马秀哲的 square,干净到只有十几行:
// 马秀哲 square.cpp if(c=='0') dp[i][j]=min(dp[i-1][j],min(dp[i][j-1],dp[i-1][j-1]))+1; ans=max(ans,dp[i][j]);
他们的数组都开在函数外(全局区,自动清零、不会爆栈),大小一次开够(如 a[100005]);代码短、变量少、不绕弯。代码越简单,出错越少——这是高手最朴素也最厉害的秘密。
别的都可以慢慢学,这三条请先记住。
钱、计数、几个数连乘、累加很多次——只要可能超过 21 亿,就用 long long。判断整除、防溢出时,每步都 %K。这一条就能帮你把 number 补成满分、把 ball 救活。
你的 sort(a,a+1,cmp) 笔误、square 的包围盒错误,只要自己拿一个小例子(比如那个 4×4 反例)跑一遍、对一下答案,当场就能发现。交之前花 1 分钟手动验一个小数据,比交完丢分强一百倍。
game 直接输出 -1 太可惜了——先写个能跑的笨办法也能多拿分。同时记住 square 那「飘忽不定的 20 分」的教训:我们要的是无论在哪台机器都正确的代码,而不是碰巧蒙对的代码。