一、关于乱搞
乱搞:通过一些方式 尝试 得到不该得到的分数
为什么要写乱搞:1. 正解不会 2. 正解太难写/正解太难调/正解难写又难调 3. 考试时间还有15min 4. 自信感觉能水过去
乱搞包括但不限于:写个非完美算法、写个 O(松) 暴力、瞎搜一通、把几个贪心拼起来、猜想出题人造的数据满足某些性质。
二、非完美算法
适用于大部分最优化问题。
- 退火:正确率高
- 爬山:收敛快
- 遗传(貌似不大行,我也不会 qwq)
如何选取:① 原则:两个都写然后对拍看哪个效果好。② 定性分析:收敛慢的(题目)爬山,收敛快的(题目)退火。
1. 爬山算法
伪代码:
ans=gen(); //随机一个初始解 for t=1 to lim do { int tmp=move(ans); //在当前解的附近随机一个新的解 ans=max(ans,tmp); //打擂 ¿ } print(ans); //输出解
两个问题:
① 正确率过低
- 多次选取初始解运行算法取最优值。
- 改成退火。
- 放弃。
② 如何在解的附近生成新解
-
解是一个数:ans+=rand(-lim,lim),lim*=1-eps。
- 解是一个串:截取子串、循环移位、修改其中一个字符、交换其中两个字符。
- 解是乱七八糟的东西:放弃。
2. 模拟退火
对爬山算法改动一个地方: 若新解劣于当前解,以 (1-eps)t 的概率接受。
伪代码:
data ans=gen(); int p=Rnd_Max; for t=1 to lim do { data tmp=move(ans); if(tmp>ans or rand()>p) ans=tmp; p*=1-eps; } print(ans);
eps 取多少:一般来说 (1-eps)lim 大约取到 1e-5。
收敛慢:eps 改大,在 t 大的时候相当于爬山。
3. 结合两种方法
用退火生成数个初始解,对每个初始解进行爬山以快速收敛。
三、O(松) 暴力
适用于已经存在一个在可观时间内能跑出更大数据范围的代码时。
1. 压位:
A. 把每 32 个位压到一个 int 里去 B. 把每 64 个位压到一个 long long 里去 C. 使用 bitset(速度 B>A>C)
手写 bitset:① 如何 | & ^ >> <<:¿ ② 如何 bitcount:查表 ③ 如何可持久化:分块
2. 缓存优化:A. 大量访问的数组能开多小开多小 B. 滚动数组
3. 内存连续访问优化:矩阵乘法/floyd
4. 循环展开:
//原来: for(i=1;i<=n;i++) do sth; //循环展开后: for(i=1;i<=n;i+=8){ do sth; do sth; do sth; do sth; do sth; do sth; do sth; do sth; } for(;i<=n;i++) do sth;
5. 变量运算速度优化:优化取模,long long 的运算变成 int 运算,struct 封装去掉,临时变量开 register,能三目不要 if
6. 输入输出优化:一般 getchar() 和 putchar() 已经够用了。当然还有 fread 和 fwrite。
读入优化模板(fread 只需把注释去掉即可) :
//#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++) //char buf[1<<23],*p1=buf,*p2=buf,obuf[1<<23],*O=obuf; template<typename T> inline void read(T& x){ x=0;int f=1; char ch=getchar(); while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();} while(isdigit(ch)) x=(x<<3)+(x<<1)+(ch-'0'),ch=getchar(); x*=f; }
四、瞎搜一通
适用于数据范围不大的题目(一般数据范围<20)
- 剪枝:不用多讲,减枝减错掉也问题不大(¿)
- 估价函数:A.严格优于解 B.严格劣于解 C.既不也不 D.等于解(B>A>C)
- 迭代加深:字面意思。“搜索层数”可以灵活加权
五、题目特殊性质
这个就没什么好写的了,主要是(一看数据就很难造的)字符串题。
六、例题
1. 「HAOI 2006」均分数据
题目大意:已知 n 个正整数 a1,a2,...,an。今要将它们分成 m 组,使得各组数据的数值和最平均,即各组的均方差最小。均方差公式如下:
(displaystylesigma = sqrt{frac 1n sumlimits_{i=1}^n(overline x - x_i)^2},overline x = frac 1n sumlimits_{i=1}^n x_i)
其中 σ 为均方差,(overline x) 为各组数据和的平均值,xi 为第 i 组数据的数值和。
Solution:
每组的和越接近越好,于是把每个数加入当前和最小的组里。
每次贪心都把序列打乱,多次运算取最优解。
循环 5e5 次,正确率大大提高。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=30; int n,m,sum,a[N],x[N]; double ans=1e18,tmp,k; double solve(){ memset(x,0,sizeof(x)),tmp=0; for(int i=1;i<=n;i++){ int p=1; for(int j=1;j<=m;j++) if(x[j]<x[p]) p=j; x[p]+=a[i]; } //把每个数加入当前和最小的组里 for(int i=1;i<=m;i++) tmp+=(x[i]-k)*(x[i]-k); return tmp*1.0/m; } signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=n;i++) scanf("%lld",&a[i]),sum+=a[i]; k=1.0*sum/m; for(int i=1;i<=5e5;i++){ random_shuffle(a+1,a+1+n); tmp=solve(),ans=min(ans,tmp); } printf("%.2lf ",sqrt(ans)); return 0; }
2. 「JSOI 2004」平衡点 / 吊打XXX
题目大意:给出平面中的 n 个点,求这 n 个点的广义费马点(费马点:在三角形内到各个顶点距离之和最小的点)。
Solution:
这道题需要一些物理知识,于是啥都不会的我看懵了(
大概是这样的:
涉及力的正交分解。如图所示,将力 F 沿力 x、y 方向分解,可得:
( egin{cases} F_x=F cos heta\ F_y=F sin heta end{cases} Rightarrow F=sqrt{F_x^2+F_y^2} )
(以上只是稍微提一下力的正交分解)
我们可以确定一个原点,将所有的力在这个原点上正交分解,最终得到所有的力的一个合力,而平衡点一定在合力所指向的方向。
每当分得到一个合力之后,将原点在合力的方向上位移一定的距离。
因为绳结不断移动的过程中,系统是不断趋向平衡的,因此每次移动的长度会不断缩小,当移动长度缩小到无法改变最终结果时输出当前位置,结束。
(我也不大懂,反正大概就酱紫了。赶紧去学物理 QwQ)
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e4+5; int n,x[N],y[N],w[N]; double ansx,ansy,fx,fy,F; void solve(double l){ fx=fy=0; for(int i=1;i<=n;i++){ //将绳结上的力正交分解 double p=sqrt((x[i]-ansx)*(x[i]-ansx)+(y[i]-ansy)*(y[i]-ansy)); if(p==0) continue; fx+=w[i]*(x[i]-ansx)/p; //fx:水平合外力 fy+=w[i]*(y[i]-ansy)/p; //fy:竖直合外力 } F=sqrt(fx*fx+fy*fy); //F:合外力,(fx/F,fy/F)为合外力的方向向量 ansx+=l*fx/F,ansy+=l*fy/F; //向合力方向移动 l } signed main(){ scanf("%lld",&n); for(int i=1;i<=n;i++) scanf("%lld%lld%lld",&x[i],&y[i],&w[i]); for(double l=1e4;l>1e-5;l*=0.79) solve(l); //l:移动长度 printf("%.3lf %.3lf ",ansx,ansy); return 0; }
3. 「AHOI2014 / JSOI2014」保龄球
题目大意:点此看题
Solution:
每次交换当前排列的两个位置,多次运算取最优解。
循环 5e5 次,正确率大大提高。(事实上 1e5 也能过)
#include<bits/stdc++.h> #define int long long using namespace std; const int N=60; int n,sum,flag,x,y,tmp,ans; struct data{ int x,y; }a[N]; int solve(){ int sum=0; for(int i=1;i<=n+flag;i++){ sum+=a[i].x+a[i].y; if(a[i-1].x==10) sum+=a[i].x+a[i].y; //全中:下一轮的得分将会被乘2计入总分 else if(a[i-1].x+a[i-1].y==10) sum+=a[i].x; //补中:下一轮中的第一次尝试的得分将会以双倍计入总分 } return sum; } signed main(){ srand(time(0)); scanf("%lld",&n); for(int i=1;i<=n;i++) scanf("%lld%lld",&a[i].x,&a[i].y); if(a[n].x==10) scanf("%lld%lld",&a[n+1].x,&a[n+1].y),flag=1; ans=solve(); for(int i=1;i<=1e5;i++){ x=rand()%(n+flag)+1,y=rand()%(n+flag)+1; while(x==y||(flag&&(x==n||y==n))) x=rand()%(n+flag)+1,y=rand()%(n+flag)+1; swap(a[x],a[y]),tmp=solve(); //每次交换当前排列的两个位置 if(tmp>=ans) ans=tmp; else swap(a[x],a[y]); } printf("%lld ",ans); return 0; }
4. CF914F Substrings in a String
题目大意:维护一个字符串 S,支持以下操作:
- 修改 S 中一个位置的字符
- 询问串 T 在 S[l..r] 中出现次数
|S|≤105,Σ|T|≤105,字符集 26。
Solution:
时限是 6s,想到可以用暴力的 bitset 维护。
用一个 bitset 的二维数组记录 S 中每个字母的位置信息,再把串 T 中每个字母与 S 的字母进行比较。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e5+5,M=30; int q,len,opt,x,l,r; char s[N],c,t[N]; bitset<N>b[M],ans; signed main(){ scanf("%s%lld",s+1,&q),len=strlen(s+1); for(int i=1;i<=len;i++) b[s[i]-'a'][i]=1; //记录每个字母的位置信息 (s[i] 在第 i 位出现过) while(q--){ scanf("%lld",&opt); if(opt==1){ scanf("%lld %c",&x,&c); b[s[x]-'a'][x]=0,b[c-'a'][x]=1,s[x]=c; } else{ scanf("%lld%lld%s",&l,&r,t+1),ans.set(),len=strlen(t+1); //ans.set():将整个 bitset (ans)设置成 1 for(int i=1;i<=len;i++) ans&=b[t[i]-'a']>>(i-1); //比较完一个就右移一位把是否包含该串的信息保存在一个位置 //ans 中有一个 1 就代表以这个位置开头包括一个串 t printf("%lld ",max(0ll,(int)(1ll*(ans>>l).count()-1ll*(ans>>(r-len+2)).count()))); //计算 l 右边和 r 右边有多少个串 t } } return 0; }
5. CF896E Welcome home, Chtholly
题目大意:维护一个序列,支持以下操作:
- 把一个区间中 >x 的数都减掉 x
- 询问一个区间中有几个 x
n≤105,q≤105,值域 105。
Solution:
暴力+卡常即可。需要亿点点信仰。
#pragma comment(linker,"/stack:200000000") #pragma GCC optimize("Ofast,no-stack-protector") #pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native") #include<bits/stdc++.h> #define re register using namespace std; const int N=1e5+5; int n,m,a[N]; float x; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]); while(m--){ re int opt,l,r,ans=0; scanf("%d%d%d%f",&opt,&l,&r,&x); if(opt==1){ for(re int i=l;i<=r;i++) a[i]-=(a[i]>x)?x:0; } else{ for(re int i=l;i<=r;i++) ans+=(a[i]==x); printf("%d ",ans); } } return 0; }
接下来讲一下正解。(¿)
值域范围小,我们可以直接记录 cnt[i][j] 表示块 i 中值为 j 的数个数。
然后再用并查集把同一块中数值相同的元素缩在一起。
对于第一个操作:首先两边的散块可以暴力修改。
对于中间的块:设 mx 表示该块中的最大值。分以下几种情况:
1. x≥mx,此时不用做任何修改
2. x<mx<2*x,此时可以暴力把区间 [x+1,mx] 和 [1,mx-x] 对应的元素合并
3. 2*x≤mx,我们注意到:第一个操作等价于先把所有元素减 x,然后把小于等于 0 的加 x。所以我们可以设一个 tag[i] 表示块 i 中这种情况减少的总数。对于这种情况,先暴力把区间 [1,x] 和 [x+1,x+x] 对应的元素合并,然后 tag[i]+=x
第二个操作:两边的散块暴力查询,中间的 ans+=cnt[i][x+tag[i]] 即可。
我们发现,对于修改操作:每一块合并的次数不超过 100000 次。所以整体时间复杂度是 O(n1.5)。