算法竞赛进阶指南例题(第一章)

算法竞赛进阶指南例题(第一章)

- 23 mins

注:本md中的/ww指(doge),/youl指幽灵

基本算法

位运算

异或
and,& or,| not,~ xor,^(C++)

不要问为什么这么简单的知识还要放,因为有一届蓝桥杯考的异或,我就忘了是哪个符号/youl

0x表示十六进制,0x00~0xFF

int 最大取值:2 147 483 647 (≈2*1e9)(0x 7F FF FF FF)

memset(a,val,sizeof(a)) 原理:把数值val(以十六进制形式)填充到数组a的每个字节上(可用0x3f给a赋值为较大数字)

0x3F 3F 3F 3F二倍不超过0x7F FF FF

算术右移(n»1) 等于除以2向下取整 n/2为向零取整

操作(n为整数) 运算
取n在二进制下的第k位 (n»k) & 1
取n在二进制下的第0到k-1位 n & ((1«k)-1)
把n在二进制下的第k位取反 n xor (1«k)
对n在二进制下的第k位赋值为1 n | (1«k)
对n在二进制下的第k位赋值为0 n & (~(1«k))

n为偶数时,n xor 1 等于n+1

n为奇数时,n xor 1 等于n-1 用于图论邻接表中边集的存储(还没学到,不懂)

lowbit运算(最低位的1及其后面所有的0)

lowbit(n) = n&(~n+1) = n&(-n)

任意k∈[0,35],2^k%37的值互不相等,且恰好取遍整数1~36

步入正题,例题讲解阶段

a^b

一道模板题,讲解快速幂的

#include <bits/stdc++.h>
using namespace std;

int power(int a, int b, int p)//快速幂函数模板,原理为位运算
{
    int ans = 1 % p;//感觉,这个%p应该可以去掉,但,emmm,加上就加上了,加上就懒得去了/ww
    for (; b; b >>= 1)
    {
        if (b & 1)
            ans = (long long)ans * a % p;//这里一定要加上那个long long,当时做的时候,因为这东西,卡了两个小时,原因就是因为是先乘再取余,所以可能会越界,下同
            
        a = (long long)a * a % p;
    }
    return ans;
}//从b的二进制的第一位开始计算,每一层,a肯定都要翻倍,如果b在该位为1,则答案即次方之后的值也要乘以当时的a(只是说一下自己的理解,后同,详细的当然还是要看书了/ww)

int main()
{
    long long a, b, p;
    cin >> a >> b >> p;
    cout << mul(a, b, p);
    return 0;
}

优化的快速幂算法(免除了两个long long数相乘超出范围的可能)

//快速乘
long long qmul(long long a, long long b, long long c)
{
    long long res = 0;
    while (b)
    {
        if (b & 1)
            res = (res + a) % c;
        a = (a + a) % c;
        b >>= 1;
    }
    return res;
}
// 快速幂
long long power(long long a, long long b, long long c)
{
    long long res = 1;
    while (b)
    {
        if (b & 1)
            res = qmul(res, a, c);
        a = qmul(a, a, c);
        b >>= 1;
    }
    return res;
}

64位整数乘法

模板题同,讲快速乘

#include <bits/stdc++.h>
using namespace std;

long long mul(long long a, long long b, long long p)//原理同位运算,emmm,感觉,都差不多,反正,我感觉都差不多,pass(方法2没看/ww)
{
    long long ans = 0;
    for (; b; b >>= 1)
    {
        if (b & 1)
            ans = (ans + a) % p;
        a = a * 2 % p;
    }
    return ans;
}

int main()
{
    long long a, b, p;
    cin >> a >> b >> p;
    cout << mul(a, b, p);
    return 0;
}

最短Hamilton路径

解释二进制状态压缩的例题,一般用于枚举

#include <bits/stdc++.h>
using namespace std;

int f[1 << 20][20];//第一个下标为枚举的状态,第二个下标为当前所在的点
// Hamilton路径的定义:从0到n-1不重不漏地经过每一点恰好一次
int hamilton(int n, int weight[20][20])
{
    memset(f, 0x3f, sizeof(f));//这里就用到了上面提到的0x3f
    f[1][0] = 0;
    for (int i = 1; i < (1 << n); i++)枚举每一个状态
    {
        for (int j = 0; j < n; j++)
            if (i >> j & 1)//寻找可能的这次经过的点
                for (int k = 0; k < n; k++)
                    if (((i ^ 1 << j) >> k) & 1) //寻找可能从哪个点来,并从中选取最小值
                        f[i][j] = min(f[i][j], f[i ^ (1 << j)][k] + weight[k][j]);
    }
    return f[(1 << n) - 1][n - 1];
}
int main()
{
    int n;
    cin >> n;
    int weight[20][20];
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cin >> weight[i][j];
    cout << hamilton(n, weight);
    return 0;
}

起床困难综合症

看题就知道需要用到位运算,但也需要一定的思考,不能去枚举所有可能的攻击力(会超时)

正确的思路是用全1和全0来判断,然后通过结果判断可以造成最大伤害的最小攻击力

#include <bits/stdc++.h>
using namespace std;

pair<string, int> a[100005]; // 说实话,第一次接触到pair,好优秀,省的去定义结构体了/ww,在我之后的代码里也经常用到(不懂可以CSDN/ww)
int n, m;
int calc(int bit, int now)//攻击力向伤害值的转化
{
    for (int i = 1; i <= n; i++)
    {
        int x = a[i].second >> bit & 1;
        if (a[i].first == "AND")
            now &= x;
        else if (a[i].first == "OR")
            now |= x;
        else
            now ^= x;
    }
    return now;
}
int main()
{

    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        char str[5];
        int x;
        cin >> str >> x;
        a[i] = make_pair(str, x);
    }
    int ans = 0, val = 0;
    for (int bit = 29; bit >= 0; bit--)
    {
        int res0 = calc(bit, 0);
        int res1 = calc(bit, 1);//按位进行转换
        if (val + (1 << bit) <= m && res0 < res1)//判断是否需要以及是否可以加对应位上的1
            val += 1 << bit, ans += res1 << bit;
        else//否则按0来加(0对于攻击力无负担)
            ans += res0 << bit;
    }
    cout << ans << endl;
    return 0;
}

递推与递归

枚举形式 状态空间规模 一般遍历方式
多项式 n^k,k为常数 for循环,递推
指数 k^n,k为常数 递归,位运算
排列 n! 递归,C++ next_permutation(下一个排列方式)
组合 Cmn(不会打) 递归+剪枝

递归实现组合型枚举

#include <bits/stdc++.h>
using namespace std;

vector<int> chosen;
int n, m;
void calc(int x)
{
    if (chosen.size() > m || chosen.size() + (n - x + 1) < m) // 剪枝,并保证输出的数列均为m个
        return;
    if (x == n + 1)
    {
        for (int i = 0; i < chosen.size(); i++)
        {
            cout << chosen[i] << ' ';
        }
        cout << '\n';
        return;
    }

    chosen.push_back(x);
    calc(x + 1);
    chosen.pop_back();
    calc(x + 1);
}
int main()
{
    cin >> n >> m;
    calc(1);
    return 0;
}

费解的开关

感觉,从这道题感觉难点还在,能不能想到,想到了其实是比较简单的,尤其是这个固定第一行,之后就按第一行的来,感觉,这是重难点.

至于想到之后解题方法,那就是枚举吧

#include <bits/stdc++.h>
using namespace std;
bool a[10][10];
bool b[10][10];
int minn;
int xx[5] = {1, 0, -1, 0, 0};
int yy[5] = {0, -1, 0, 1, 0};
void click(int x, int y)
{
    for (int i = 0; i < 5; i++)
    {
        a[x + xx[i]][y + yy[i]] = !a[x + xx[i]][y + yy[i]];
    }
}
int main()
{

    int n;
    cin >> n;
    while (n--)
    {
        minn = 10;
        for (int i = 1; i <= 5; i++)
            for (int j = 1; j <= 5; j++)//输入函数,因为是连着的,所以需要用char转一下
            {
                char aa;
                cin >> aa;
                if (aa == '1')
                    b[i][j] = 1;
                else
                    b[i][j] = 0;
            }
        int sum = 0;
        for (int i = 0; i < 1 << 5; i++) //这里确定第一行的方法和上面那个最短hamilon?差不多路径是一样的,通过二进制来判断
        {
            sum = 0;
            for (int j = 0; j <= 6; j++)
                for (int k = 0; k <= 6; k++)
                    a[j][k] = b[j][k];
            for (int j = 0; j < 5; j++)//确定第一行点击后的的初始状态
                if ((i >> j) & 1)
                {
                    click(1, j + 1);
                    sum++;
                }
            for (int j = 1; j < 5; j++)
            {
                for (int k = 1; k <= 5; k++)
                {
                    if (!a[j][k])
                    {
                        click(j + 1, k);
                        sum++;
                    }
                }
            }
            bool jud = 1;
            for (int j = 1; j <= 5; j++)//判断第5行是否全1
            {
                if (!a[5][j])
                    jud = 0;
            }
            if (jud)
                minn = min(minn, sum);
        }
        if (minn <= 6)
            cout << minn << '\n';
        else
            cout << "-1\n";
    }
    return 0;
}

Strange Towers of Hanoi

感觉,数学规律题,

三盘时的d[n] = 2* d[n-1] + 1;

四盘时的f[n] = min(1<=i<n){2*f[i] + d[n-i]}

需要考虑一下过程,属于,经典的递推题

(好像,没存下来,不写了,相信你们会的)

Sumdiv

分治问题(废话,能想到那个规律,谁不知道是分治)

感觉,也是,看基础的一道题,属于是,知道那些公式就比较容易,不知道,hh,下道题吧

A分解质因数的公式,再进一步分析A^B的约数公式,再进一步分析A^B的所有约数之和(其中,第一个公式应该记住,后面两个,可推导(当然,也可记))

然后就是这道题本意要考察的点了,也就是,分治法求等比数列,通过递归分治,不断缩小问题规模,配合快速幂,在O(logc)时间内求出等比数列的和

#include <bits/stdc++.h>
using namespace std;
vector<pair<int, int>> vec; //pair yyds
const int q = 9901;
int power(int a, int b)//快速幂模板
{
	int ans = 1 % q;
	for (; b; b >>= 1)
	{
		if (b & 1)
			ans = (long long)ans * a % q;
		a = (long long)a * a % q;
	}
	return ans;
}

int sum(int p, int c)//书上公式的代码实现(自我建议,记下来(虽然,我也还没记下来吧/ww))
{
	if (c == 0)
		return 1;
	else
		;
	if (c % 2)
		return ((1 + power(p, (1 + c) / 2)) * sum(p, (c - 1) / 2)) % q;
	else
		return ((1 + power(p, c / 2)) * sum(p, (c / 2) - 1) + power(p, c)) % q;
}

int main()
{
	int a, b;
	cin >> a >> b;
	for (int i = 2; i <= a; i++)//找质因数
	{
		if (!(a % i) && a)
		{
			int cnt = 0;
			while (!(a % i))
			{
				a /= i;
				cnt++;
			}
			vec.push_back(make_pair(i, cnt * b));
		}
	}
	if (a != 1)//找质因数同
	{
		vec.push_back(make_pair(a, b));
	}

	int ans = 1;
	for (int i = 0; i < vec.size(); i++)//将各个等比数列相加
	{
		ans *= sum(vec[i].first, vec[i].second);
		ans %= q;
	}
	cout << ans;
	return 0;
}

前缀和与差分

概念嘛,自己看书/ww,就两个公式

差分,有助于把原序列上的区间操作转化为差分序列上的单点操作

上例题

激光炸弹

讲真,看这道题,真就,长见识,真就,没想到前缀和还能这么用

#include <bits/stdc++.h>
using namespace std;

int s[10005][10005];
int a[10005][10005];
int main()
{
    int n, r;
    cin >> n >> r;
    for (int i = 0; i < n; i++)//x++,y++是为了,方便下面的计算,省的处理越界问题了
    {
        int x, y;
        cin >> x >> y;
        x++;
        y++;
        cin >> a[x][y];
    }

    s[1][1] = a[1][1];
    for (int i = 1; i <= 5000; i++)
        for (int j = 1; j <= 5000; j++)
            s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
    int maxx = 0;
    for (int i = r; i <= 5000; i++)
        for (int j = r; j <= 5000; j++)
            maxx = max(maxx, s[i][j] - s[i - r][j] - s[i][j - r] + s[i - r][j - r]);
    cout << maxx;
    return 0;
}

IncDec Sequence

这道题就,理解,然后懂差分的这个公式,应该就,问题不大?

#include <bits/stdc++.h>using namespace std;int main(){    int n;    cin >> n;    long long a[100005], b[100005];    cin >> a[0];    b[0] = a[0];//这里可以选择特殊处理一下0,也可以和上面的激光炸弹一样,设置为全局变量,然后从1开始    long long zheng = 0, fu = 0;    for (int i = 1; i < n; i++)    {        cin >> a[i];        b[i] = a[i] - a[i - 1];//差分序列初始化        if (b[i] > 0)            zheng += b[i];        else            fu -= b[i];//计算正数和负数的个数    }    b[n] = 0;    cout << max(zheng, fu) << '\n';    cout << abs(zheng - fu) + 1;        return 0;} 

Tallest Cow

感觉这道题比上题简单,就, 纯纯差分的应用,不过要特殊主义的就是,可能会出现相同的两头牛,用一个bool的二维数组判断下就好

#include <iostream>
using namespace std;
int c[10005], d[10005];
bool jud[10005][10005];
int main()
{
    int n, i, h, r;
    cin >> n >> i >> h >> r;
    while (r--)
    {
        int a, b;
        cin >> a >> b;
        if (a > b)
            swap(a, b);
        if (jud[a][b])
            continue;
        d[a + 1]--;
        d[b]++;
        jud[a][b] = 1;
    }
    for (int i = 1; i <= n; i++)
    {
        c[i] = c[i - 1] + d[i];
        cout << h + c[i] << '\n';
    }
    return 0;
}

二分

二分优秀啊二分,二分以及下下节的倍增,都是属于,让人眼界一新的查找方法,就,膜拜大佬(指想出来的那位)

三分求单峰函数,l和r取二等分点极其接近的地方,每次缩小1/2的范围

答案具有单调性(最大值最小问题),可用二分转化为判定(直至缩小到l=r),把求最优解问题转化为判定是否存在可行方案使结果达到mid的问题

先放下二分的模板吧,

整数集合上

while(l<r)
{
    int mid = (l+r)>>1;
    if(a[mid]>=x) r=mid;
    else l=mid+1;
}
return a[l];
while(l<r)
{
	int mid = (l+r+1)>>1;
	if(a[mid]<=x) l=mid;
    else r=mid-1;
}
return a[l];
在互不相等的情况下
while(l<=r)
{
    int mid=(l+r)>>1;
    if(compare(res[mid],i))l=mid+1;//因为不可能会等于,所以可以这样写
    else r=mid-1;
}
return r;//插入在r后面

实数域

while(l+1e-5<r)
{
	double mid = (l+r)/2;
	if(calc(mid)) r = mid;
    else l = mid;
}

for(int i=0;i<100;i++)
{
    double mid = (l+r)/2;
	if(calc(mid)) r = mid;
    else l = mid;
}

Best Cow Fences

在看这本书之前,甚至连不限长最大字段和都没有,固定的思维去写

而且,这个取min_val的方法,也是优秀,感觉,充分说明了,编程题中,公式的重要性,没有公式,有时候真的就,思路会很乱

#include <bits/stdc++.h>
using namespace std;

double a[100005], b[100005], sum[100005];
int main()
{
    int n, f;
    cin >> n >> f;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    double eps = 1e-5;
    double l = -1e6, r = 1e6;
    while (r - l > eps)//实数域的二分
    {
        double mid = (l + r) / 2;
        for (int i = 1; i <= n; i++)
        {
            b[i] = a[i] - mid;
            sum[i] = sum[i - 1] + b[i];
        }
        double ans = -1e9;
        double min_val = 1e9;
        for (int i = f; i <= n; i++)
        {
            min_val = min(min_val, sum[i - f]);
            ans = max(ans, sum[i] - min_val);
        }
        if (ans >= 0)//说明可以取到该值
            l = mid;
        else
            r = mid;
    }
    cout << int(r * 1000);
    return 0;
}

Innovative Business

// Forward declaration of compare API.
// bool compare(int a, int b);
// return bool means whether a is less than b.

//函数题
class Solution {
public:
    vector<int> specialSort(int N) {
        vector<int>res;
        res.push_back(1);
        for(int i=2;i<=N;++i){
            int l=0,r=res.size()-1;
            while(l<=r){
                int mid=(l+r)>>1;
                if(compare(res[mid],i))l=mid+1;//因为不可能会等于,所以可以这样写
                else r=mid-1;
            }
            res.push_back(i);
            for(int j=res.size()-2;j>r;j--)
            swap(res[j],res[j+1]);
        }
        return res;
    }
};

排序

九大排序算法

选择,插入,冒泡

堆,归并,快排 属于是,要难理解一些,但复杂度比前三个要好,优化了logn

计数,基数,桶

最后三个是真优秀,真就,可能能达到O(n)的复杂度了已经!

离散化:把无穷大集合(尤其指数非常分散的情况)中的若干个元素映射为有限集合(中连续的数)以便于统计(和排序)的方法

离散化模板

void discrete()
{
    sort(a+1,a+n+1);
    for(int i=1;i<=n;i++)
        if(i==1||a[i]!=a[i-1]) 
            b[++m]=a[i];
}

int query(int x)
{
    return lower_bound(b+1,b+m+1,x) - b;
}

第k大数没有题/youl,感觉自己看明白了,嗯,对,看明白了(用快排)

逆序对用归并(or树状数组?是个什么东西)

模板

void merge(int l, int mid, int r)
{
    if (l == r)
        return;
    merge(l, (l + mid) / 2, mid);
    merge(mid + 1, (mid + 1 + r) / 2, r);
    int i = l, j = mid + 1;
    for (int k = l; k <= r; k++)
        if (j > r || i <= mid && a[i] <= a[j])
            b[k] = a[i++];
        else
            b[k] = a[j++], cnt += mid - i + 1;
    for (int k = l; k <= r; k++)
        a[k] = b[k];
}

Cinema

一道,考察离散化?嗯,算是吧,毕竟,好像不离散化的话,就需要按别的方法来写,or,超时,以及考察思维的题

image-20220816094458890

给各位增强了下数据,你们不要感谢我/ww,乐

#include <bits/stdc++.h>
using namespace std;

bool cmp(pair<int, int> a, pair<int, int> bb)//这里需要注意的是,重载运算符是不可行的,因为存在系统定义的对于pair的<运算
{
    return a.second > bb.second;
}

bool jud[200005];
int a[200005];//储存离散化之前的原始数据
pair<int, int> b[200005];//储存离散化之后的数据,其中,first表示对应的原始数据,second表示所存在的个数
map<int, bool> mapp;
map<int, bool> mappp;//两个map用来判断是否该数字在科学家?人?所懂得的语言之中,两个map是因为,要分别对语音和字幕判断
int n, m;
int main()
{

    cin >> n;

    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        mapp[a[i]] = 1;
        mappp[a[i]] = 1;
    }
    sort(a + 1, a + n + 1);//进行离散化处理
    int t = 0;
    for (int i = 1; i <= n; i++)
    {
        if (i == 1 || a[i] != a[i - 1])
        {
            b[++t].first = a[i];
            b[t].second = 0;
        }
        b[t].second++;
    }
    sort(b + 1, b + t + 1, cmp);//根据个数排序(从大到小)
    cin >> m;
    int c[200005], d[200005];
    int q = t;
    vector<int> ans;
    for (int i = 1; i <= m; i++)
    {
        cin >> c[i];
        if (!mapp[c[i]])//如果没有人懂,则直接pass,因为懂的人为0,不会为最大,(如都不懂会有特殊处理)
            continue;
        bool judd = 0;
        for (int j = q; b[j].second == b[q].second; j--)//从最后一个,也就是最小的开始处理
        {
            if (c[i] == b[j].first)//如果个数同最小的,则judd = 1,然后向这时的ans中push
            {
                judd = 1;
                break;
            }
        }
        if (judd)
            ans.push_back(i);
        else//否则说明是该语音适合更多人
        {
            while (c[i] != b[q].first)//直到找到对应的那个q,无需考虑q<0,因为不会存在,不过,鲁棒性的话,emmm,可加
            {
                mapp[b[q].first] = 0;//令那些比当前适配适应人数少的map都变为0,这也是要开两个map的原因(因为数据有修改)
                q--;
            }
            ans.clear();//建立新的答案vector
            ans.push_back(i);
        }
    }
    for (int i = 0; i < ans.size(); i++)//如果语音不属于答案范围,就直接pass
    {
        jud[ans[i]] = 1;
    }
    q = t;
    vector<int> anss;//这里新开一个是为了之后的判断,即之前的ans数组还有用
    for (int i = 1; i <= m; i++)//整体的判断逻辑同上
    {
        cin >> d[i];
        if (!mappp[d[i]])
            continue;
        else if (!jud[i])
            continue;
        else
            ;
        if (d[i] == b[q].first)
            ans.push_back(i);
        else
        {
            while (d[i] != b[q].first)
            {
                mappp[b[q].first] = 0;
                q--;
            }
            anss.clear();
            anss.push_back(i);
        }
    }
    if (!anss.empty())//这里要注意的是,可能会存在anss甚至ans为空的情况,所以要用两个empty函数判断,分三种情况
        cout << anss[0];//这里其实输出现在的anss数组中的任意一个均可,但,懒得查找大小,就直接输出第一个了/ww
    else if (!ans.empty())
        cout << ans[0];
    else
        cout << 1;//如果都为空,说明所有电影都一样(都是没人听懂也没人看懂),那就,摆烂/ww,直接输出第一部电影
    return 0;
}

/*
	!!写题解的时候,忽然想到一个bug,就是我的方法好像,如果都没人听懂,那就直接摆烂随便选了,好像也不看有没有人能看懂了/youl
			但是,accept了当时,真的,emmm,在acwing上,这就去申请加强数据/ww
	
	解决方法就是,在进行字幕的判断的时候,先判断一下ans是否为空,如果为空,那就把所有的jud赋值为true
	即
	for (int i = 0; i < ans.size(); i++)//如果语音不属于答案范围,就直接pass
    {
        jud[ans[i]] = 1;
    }
    --->
    if(ans.empty())
    for(int i=0;i<=t;i++) jud[i] = 1;
    else
    for (int i = 0; i < ans.size(); i++)//如果语音不属于答案范围,就直接pass
    {
        jud[ans[i]] = 1;
    }
*/

货仓选址

感觉就是一道,说明中位数的,模板题?反正就,属于那种想到了就极其容易,想不到就,拜拜的题,乐

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    int a[100005];
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    sort(a + 1, a + n + 1);
    int ave = a[n / 2 + 1];//这里属于是,懒省事了,就,奇数偶数一起来,到时候如果没记住的话,其实这种东西,可以举例子判断的/ww
    long long sum = 0;
    for (int i = 1; i <= n; i++)
        sum += abs(a[i] - ave);
    cout << sum;
    return 0;
}

七夕祭

这道题,当时还没写的时候,看到题目的第一感觉就是难(莫名的感觉)

不过,在学完前置知识,并且看了书上的思路之后,倒也没感觉有多难了,不过,如果二者缺一,那,emmm,寄

讲真,均分纸牌问题,也属于是,之前没了解过,写了这道题才知道的

这道题的话,第一点要拆分成两个互相独立的部分来写

第二点,想到了均分纸牌问题,找到相似之处,并且熟悉均分纸牌问题的解法

明白环形均分纸牌问题和普通均分纸牌问题的区别,并且理解了本题出题的本意,也就是选取中位数

如书中所说,此题本来是一道看着比较难的题,但通过一步步的分析,拆解,类比,最终变换为了一些经典问题的集合

#include <bits/stdc++.h>
using namespace std;
int row[100005], col[100005];
int main()
{
    int n, m, t;
    cin >> n >> m >> t;

    for (int i = 0; i < t; i++)
    {
        int x, y;
        cin >> x >> y;
        row[y]++;//计算每行/列所包含的个数
        col[x]++;
    }
    bool jud_row = 1, jud_col = 1;
    if (t % n)//如果不能整除n或m则肯定无法达到每行/列相同数量的结果
        jud_row = 0;
    if (t % m)
        jud_col = 0;
    long long cnt = 0;
    if (jud_row)
    {
        int ave = t / n;
        long long s[100005];
        s[0] = 0;
        for (int i = 1; i <= n; i++)//防治越界,所以从1开始
        {
            row[i] -= ave;
            s[i] = s[i - 1] + row[i];
        }
        sort(s + 1, s + n + 1);
        int ve = n / 2 + 1;//计算中位数
        for (int i = 1; i <= n; i++)
        {
            cnt += abs(s[i] - s[ve]);
        }
    }
    if (jud_col)
    {
        int ave = t / m;
        long long ss[100005];
        ss[0] = 0;
        for (int i = 1; i <= m; i++)
        {
            col[i] -= ave;
            ss[i] = ss[i - 1] + col[i];
        }
        sort(ss + 1, ss + m + 1);
        int ve = m / 2 + 1;
        for (int i = 1; i <= m; i++)
        {
            cnt += abs(ss[i] - ss[ve]);
        }
    }
    if (jud_col && jud_row)
        cout << "both ";
    else if (jud_col)
        cout << "column ";
    else if (jud_row)
        cout << "row ";
    else
        cout << "impossible";
    if (jud_col || jud_row)
        cout << cnt;
    return 0;
}

Running Median

啊,这道题,因为,听说要用到堆,所以,pass了,之后学完如果能想起来就再回来补/ww

奇数码问题

奇数码游戏,两个局面可达,当且仅当两个局面下网格中的数依次写成1行序列(不考虑空格)后,逆序对个数的奇偶性相同

n为偶数时,两局面可达,当且仅当两个局面下网格中的数依次写成1行序列(不考虑空格)后,逆序对数之差和两个局面下空格所在的行数之差奇偶性相同(n为行数,与列数无关)

n*m数码问题的有解性判定,可以转化为归并排序求逆序对来解决

#include <bits/stdc++.h>
using namespace std;
long long cnt;
int a[300000], b[300000];
void merge(int l, int mid, int r)//计算逆序对的模板(没错,上面的模板就是cv的这里的/ww)
{
    if (l == r)
        return;
    merge(l, (l + mid) / 2, mid);
    merge(mid + 1, (mid + 1 + r) / 2, r);
    int i = l, j = mid + 1;
    for (int k = l; k <= r; k++)
        if (j > r || i <= mid && a[i] <= a[j])
            b[k] = a[i++];
        else
            b[k] = a[j++], cnt += mid - i + 1;
    for (int k = l; k <= r; k++)
        a[k] = b[k];
}

int main()
{
    int n;
    while (scanf("%d", &n) != EOF)
    {
        if (!n)
            cin >> n;
        for (int i = 0; i < n * n - 1; i++)//数据输入进一行,并排除0
        {
            cin >> a[i];
            if (!a[i])
                cin >> a[i];
        }
        cnt = 0;
        if (n != 1)
            merge(0, (n * n - 1) / 2, n * n - 1);
        bool jud = cnt % 2;//判断奇偶性
        for (int i = 0; i < n * n - 1; i++)
        {
            cin >> a[i];
            if (!a[i])
                cin >> a[i];
        }
        cnt = 0;
        if (n != 1)
            merge(0, (n * n - 1) / 2, n * n - 1);
        if (jud == cnt % 2)//判断奇偶性是否相同
            cout << "TAK\n";
        else
            cout << "NIE\n";
        if (n == 1)//n==1的话,就是只有一个0,然后,因为上面的循环问题,不会输入,所以需要单独判断一下
            cin >> n >> n;
    }
    return 0;
}

倍增

一种对线性递推的优化方案,通过成倍增长的方式,只递推状态空间中在2的整数次幂位置上的值作为代表

ST算法也是,优秀

模板

void ST_prework()
{
	for(int i = 1;i <= n;i++) f[i][0] = a[i];
    int t = log(n) / log(2) + 1;
    for(int j = 1;j < t;j++)//区间的长度
        for(int i = 1; i <= n - (1<<j) + 1;i++)
            f[i][j] = max(f[i][j-1] , f[i + (1<<(j-1))][j-1]);
}
int ST_query(int l,int r)
{
	int k = log(r - l + r) / log(2);//orO(N)预处理一下1~N对应的k值
    return max(f[l][k] , f[r - (1<<k) + 1][k]);
}

Genius ACM

首先,肯定的一点,本题是要用到倍增的,在判断至少要分为几段的时候

但是,这道题最坑的地方,个人感觉不在倍增,而是那个计算校验值的时候,用快排会超时,当时我真的,没有计算时间复杂度,然后开始用快排写的,之后,ACwing就给我弹出了,emmm具体单词忘了,反正就是超时的意思,于是,就又有的书上说的这种类似归并排序的算法

#include <bits/stdc++.h>
using namespace std;
int ll, rr;//用ll和rr来储存上一次的左右边界
long long a[500005];
long long b[500005];
long long c[500005];
void merge(long long l, long long r)//这个函数就是类归并排序
{
    int now = l;
    int i = l, j = rr + 1;
    for (int k = rr + 1; k <= r; k++)
        b[k] = a[k];
    sort(b + rr + 1, b + r + 1);//要注意,这里,要给他们进行一次快排
    while (now <= r)
    {
        if (j > r || (i <= rr && b[i] < b[j]))
            c[now++] = b[i++];
        else
            c[now++] = b[j++];
    }
    for (int i = l; i <= r; i++)
        b[i] = c[i];
    rr = r;
}
long long value(long long l, long long r, long long m)
{
    long long ans = 0;
    if (l == ll && r > rr)//这里说的是,如果左边界没有变,并且r还要大于rr,就可以用这种类归并排序
        merge(l, r);
    else//如果变了,那就乖乖快排多好
    {
        for (int i = l; i <= r; i++)
            b[i] = a[i];
        sort(b + l, b + r + 1);
        ll = l, rr = r;
    }

    while (r > l && m--)//这里是对校验值的一个计算
    {
        ans += (b[l] - b[r]) * (b[l] - b[r]);
        l++, r--;
    }
    return ans;
}
long long m;
long long k;
int main()
{
    ll = 1e6;//因为ll的初值为0,而第一个,不能直接用那种类归并
    cin >> k;
    while (k--)
    {
        long long n, t;
        cin >> n >> m >> t;

        for (long long i = 0; i < n; i++)
            cin >> a[i];

        long long p = 1, r = 0, l = 0;
        long long cnt = 0;
        while (r < n)
        {
            p = 1;
            while (p)//倍增算法
            {
                if (r + p < n && value(l, r + p, m) <= t)//注意先后顺序,防止越界
                    r += p, p *= 2;
                else
                    p /= 2;
            }
            cnt++;
            l = r + 1;//下一段的起点
            r = l;
        }
        cout << cnt << '\n';
    }
    return 0;
}

贪心

使用要求问题的整体最优性可以由局部最优性导出

证明手段:

微扰(邻项交换) : 任何对局部最优策略的微小改变都会导致整体结果变差

范围缩放 : 任何对局部最优策略作用范围的扩展都不会造成整体结果变差

决策包容性 : 这个局部最优策略提供的可能性包含其他所有策略提供的可能性

反证法

数学归纳法

感觉贪心问题的话,最主要,最难想到,最难证明的就是贪心的正确性了,证明了正确性之后,其实,挺简单的

Sunscreen

书上给的是,按minSPF递减排序,然后每次选择SPF最大的防晒霜,感觉也可以maxSPF递增排序,然后每次选择SPF最小的防晒霜,原理是一样的

这道题的贪心证明,感觉应该是范围缩放?因为后面奶牛只会出现,xy,0,x三种情况(在y的SPF大于x的SPF时),所以选择y不会造成更差的结果

感觉,要学到的点就是,化整为零,这么多想不出来就两个两个来

#include <bits/stdc++.h>
using namespace std;

bool cmp(pair<int, int> a, pair<int, int> b) // 这里是习惯问题,懒得记参数,所以直接搞一个函数了
{
    return a.first > b.first;
}

int main()
{
    int c, l, q;
    cin >> c >> l;
    pair<int, int> a[2505];
    pair<int, int> b[2505];
    for (int i = 0; i < c; i++)
        cin >> a[i].first >> a[i].second;
    for (int i = 0; i < l; i++)
        cin >> b[i].first >> b[i].second;
    sort(a, a + c, cmp);
    sort(b, b + l, cmp);
    int cnt = 0, now2 = 0;
    while (now2 < c)
    {
        for (int i = 0; i < l; i++)
        {
            if (b[i].second && b[i].first >= a[now2].first && b[i].first <= a[now2].second)
            {
                cnt++;
                b[i].second--;
                break;
            }
        }
        now2++;
    }
    cout << cnt;
    return 0;
}

Stall Reservations

这道题的贪心算法是按照开始吃草的时间排序

这属于是,决策包容性?就是如果不优先给开始时间早的,那么之后肯定要新建一个畜栏给它,而在可选的情况下,肯定是优先分配已有畜栏

这破题有一个坑人的地方,就是最后还让输出每头牛对应的畜栏,导致我不能用pair,还要新开一个结构体,写个three(其实应该是third)存一开始的序号

#include <bits/stdc++.h>
using namespace std;

struct astr
{
    int first;
    int second;
    int three;
};

bool cmp(astr a, astr b)
{
    return a.first < b.first;
}
int main()
{
    int n;
    cin >> n;
    astr a[50005];
    for (int i = 0; i < n; i++)
    {
        cin >> a[i].first >> a[i].second;
        a[i].three = i + 1;
    }
    sort(a, a + n, cmp);
    vector<int> vec;
    int tmp[50005];//用来存最后哪头牛别分配到了哪个畜栏
    int now = 1;
    vec.push_back(0);//先把开始时间最早的存入(其实好像没必要)
    tmp[a[0].three] = 1;
    while (now < n)
    {
        bool jud = 0;
        for (int i = 0; i < vec.size(); i++)
        {
            if (a[vec[i]].second < a[now].first)
            {
                vec[i] = now;
                jud = 1;
                tmp[a[now].three] = i + 1;
                break;
            }
        }
        if (!jud)
        {

            vec.push_back(now);
            tmp[a[now].three] = vec.size();
        }

        now++;
    }
    cout << vec.size() << '\n';
    for (int i = 1; i <= n; i++)
        cout << tmp[i] << '\n';
    return 0;
}

Radar Installation

感觉这类题型好像之前也遇到过(好像还是我校的招新赛,好像又没出,忘了/youl,当然,没做出来,当时连转化都没想到)

对于这道题,首先第一点就是要计算出每个建筑物对应的x轴上的点,将问题转化为N个区间内选择最少的点,使每个区间包含至少一个点

然后,就是贪心算法,按左端点从小到大排序,证明就也是决策包容性,首先可以肯定的是要排序,然后如果对左端点排序就从最左边开始,对右端点排序的话就从最右边开始,(其实应该也是可以的,不过,习惯的话,还是从左端点开始吧),然后,就是在满足当前设备的前提下,要尽可能的往后放(为了尽可能满足后面的设备,额,设备指建筑物)

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    long long d;
    cin >> n >> d;
    pair<double, double> a[1005];
    long long x, y;
    for (int i = 0; i < n; i++)
    {
        cin >> x >> y;
        if (y > d)//这里要考虑到,如果纵坐标直接大于d的话,那就pass吧,那肯定是达不到了,直接cout-1,然后return重开吧/ww
        {
            cout << "-1";
            return 0;
        }
        double q = sqrt(d * d - y * y);
        a[i].first = x - q;//这里是区间
        a[i].second = x + q;
    }
    sort(a, a + n);//pair默认的排序方式是按first值进行排序
    double pos = -1e6;
    int cnt = 0;
    int now = 0;
    while (now < n)
    {
        if (pos >= a[now].first)//这里因为开始的区间是排好序的,所以只需要对比最后面的摄像头位置是否在左端点的右面即可
            pos = min(pos, a[now].second);
        else
        {
            cnt++;
            pos = a[now].second;
        }
        now++;
    }
    cout << cnt;

    return 0;
}

国王游戏

感觉这道题的主要难点在于数学公式的分析,这里就体现出来了草稿本的重要性,有时候在纸上写写画画,思路这不就出来了/ww

哦对,还有一个离谱的难点,在于大数的乘除运算,这部分的代码是借鉴的y总的(附链接 AcWing 114. 国王游戏 - AcWing)

至于原理,其实就是,相当于模拟了竖式计算,建议当模板记下来

这个贪心算法的证明还是很容易的,吧,就微扰

#include <bits/stdc++.h>
using namespace std;

struct aaa
{
    int first;
    int second;
    int third;
};

vector<int> mul(vector<int> a, int b)
{
    vector<int> c;
    int t = 0;
    for (int i = 0; i < a.size(); i++)
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    while (t)
    {
        c.push_back(t % 10);
        t /= 10;
    }
    return c;
}
vector<int> div(vector<int> a, int b)
{
    vector<int> c;
    bool is_first = 1;
    for (int i = a.size() - 1, t = 0; i >= 0; i--)
    {
        t = t * 10 + a[i];
        int x = t / b;
        if (!is_first || x)
        {
            is_first = 0;
            c.push_back(x);
        }
        t %= b;
    }
    reverse(c.begin(), c.end());//这是一个反转函数,也就是,将c中数据反转一下
    return c;
}
vector<int> max_vec(vector<int> a, vector<int> b)//这里是比较大数大小的方法
{
    if (a.size() > b.size())
        return a;
    if (a.size() < b.size())
        return b;
    if (vector<int>(a.rbegin(), a.rend()) > vector<int>(b.rbegin(), b.rend()))
        return a;
    return b;
}
bool cmp(aaa a, aaa b)
{
    return a.third < b.third;
}

int main()
{
    int n;
    cin >> n;
    aaa pai[1005];
    cin >> pai[0].first >> pai[0].second;
    for (int i = 1; i <= n; i++)
    {
        cin >> pai[i].first >> pai[i].second;
        pai[i].third = pai[i].first * pai[i].second;
    }
    sort(pai + 1, pai + n + 1, cmp);
    vector<int> cheng(1, 1);//建立一个vector,其中包含一个元素,值为1
    vector<int> res(1, 0);//包含一个元素,值为0
    for (int i = 0; i <= n; i++)
    {
        if (i)
            res = max_vec(res, div(cheng, pai[i].second));
        cheng = mul(cheng, pai[i].first);
    }
    for (int i = res.size() - 1; i >= 0; i--)
        cout << res[i];
    return 0;
}

Color a Tree

这道题,emmm,刚开始写的时候因为看到了树,于是就不想写了,直接就,借鉴的y总的代码(链接,同 AcWing 115. 给树染色 - AcWing)

放下代码,写下注解吧

#include<bits/stdc++.h>
using namespace std;

int n,root;
struct node
{
    int p,s,v;//父节点,包含结点数,value
    double avg;
}nodes[1010];

int find()//这里是查找等效权值最大的点
{
    double avg = 0;
    int res = -1;
    for(int i=1;i<=n;i++)
    {
        if((i!=root&&nodes[i].avg > avg))
        {
            avg = nodes[i].avg;
            res = i;
        }
    }
    return res;
}

int main()
{
    cin>>n>>root;
    int ans = 0;
    for(int i=1;i<=n;i++)//初始化输入
    {
        cin>>nodes[i].v;
        nodes[i].avg = nodes[i].v;
        nodes[i].s = 1;
        ans += nodes[i].v;//这里分开计算相加就很优秀,涨知识,这里是加上最开始的值
    }
    for(int i=0;i<n-1;i++)
    {
        int a,b;
        cin>>a>>b;
        nodes[b].p = a;

    }
    for(int i=0;i<n-1;i++)//找n-1个结点(除根节点外所有)
    {
        int p =find();
        int father = nodes[p].p;
        ans += nodes[p].v*nodes[father].s;//这里是,按书上说的,放在父节点之后,那就肯定是要加上当前值乘以父节点个数的乘积
        nodes[p].avg = -1;//这里avg=-1,防止find函数二次查找
        for(int j=1;j<=n;j++)
            if(nodes[j].p == p)
                nodes[j].p = father;
        nodes[father].v+=nodes[p].v;
        nodes[father].s+=nodes[p].s;
        nodes[father].avg = (double)nodes[father].v/nodes[father].s;

    }
    cout<<ans;

    return 0;
}

好了,第一章基本算法就,到此结束了(作于2022.08.15 and 16)