從 613 的課件上摘了一堆。
一、關於亂搞
亂搞:通過一些方式 嘗試 得到不該得到的分數。
為什么要寫亂搞: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 組,使得各組數據的數值和最平均,即各組的均方差最小。均方差公式如下:
\(\displaystyle\sigma = \sqrt{\frac 1n \sum\limits_{i=1}^n(\overline x - x_i)^2},\overline x = \frac 1n \sum\limits_{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\n",sqrt(ans)); return 0; }
2. 「JSOI 2004」平衡點 / 吊打XXX
題目大意:給出平面中的 n 個點,求這 n 個點的廣義費馬點(費馬點:在三角形內到各個頂點距離之和最小的點)。
Solution:
這道題需要一些物理知識,於是啥都不會的我看懵了(
大概是這樣的:
涉及力的正交分解。如圖所示,將力 F 沿力 x、y 方向分解,可得:
\( \begin{cases} F_x=F \cos \theta\\ F_y=F \sin \theta \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\n",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\n",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\n",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\n",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)。