「算法笔记」乱搞小记


从 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