「算法筆記」亂搞小記


從 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) 

  1. 剪枝:不用多講,減枝減錯掉也問題不大(¿)
  2. 估價函數:A.嚴格優於解    B.嚴格劣於解   C.既不也不   D.等於解(B>A>C) 
  3. 迭代加深:字面意思。“搜索層數”可以靈活加權

五、題目特殊性質

這個就沒什么好寫的了,主要是(一看數據就很難造的)字符串題。

六、例題

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,支持以下操作:

  1. 修改 S 中一個位置的字符
  2. 詢問串 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)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM