学案1

前言:找的题有点少,见谅,毕竟纯考基础算法的题也不多QwQ
upd8/17:为了凑篇幅长度,为了详细一点,加上了代码。

基础算法&思想:

普通贪心

思想:
顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,希望贪心算法得到的最终结果也是整体最优的。虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如单源最短路经问题,最小生成树问题等。在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。
简结:emmm,选择局部最优。

适用问题:整体最优性可以被局部最优性推导出。(+不知道正解的题)

证明贪心方法:

  1. 微扰:即任意交换相邻两项都会使整体变差
  2. 范围缩放:扩大局部范围选取最优,不会使整体差
  3. 决策包容性:就是劣解能到达的情况,优解都能到达(优解包含劣解
  4. 反证法
  5. 数学归纳法
  6. 直觉:如果不是特别简单的题,会有坑,但仍然可以“凭天生的灵感直奔终点”。

由于正常贪心被讲过很多遍了,所以仅简单说一下。

例题

P3619 魔法

题目描述

CJB 知道是误会之后,跟你道了歉。你为了逗笑他,准备和他一起开始魔法。不过你的时间不多了,但是更惨的是你还需要完成 \(n\) 个魔法任务。假设你当前的时间为 \(T\),每个任务需要有一定的限制 \(t_i\) 表示只有当你的 \(T\) 严格大于 \(t_i\) 时你才能完成这个任务,完成任务并不需要消耗时间。当你完成第 \(i\) 个任务时,你的时间 \(T\) 会加上 \(b_i\),此时要保证 \(T\) 在任何时刻都大于 \(0\),那么请问你是否能完成这 \(n\) 个魔法任务,如果可以,输出 \(\texttt{+1}\texttt{s}\),如果不行,输出 \(\texttt{-1}\texttt{s}\)

这题的话,先想出来了一个错误的贪心(按照\(b_i\)的值从大到小排序)可是会有问题。
\(T+b_1<t_2\)然而\(T+b_2>t_1\)这时候先选择2更合适

\[t_2-b_1>t_1-b_2 \]

\[t_2+b_2>t_1+b_1 \]

排序顺序就出来啦,再按题意模拟一遍即可。

P3944 肮脏的牧师

这题挺有意思的,思考思考。

先不管三七二十一,凑到一定能A爆对方的情况,然后再考虑多出的情况,通过玩牌经验,3攻击力的是最不优秀的,1其次,2理想。所以在凑够的时候,考虑按照312的顺序还回去就ok了。

后悔贪心

浅谈思想:

先找一个错误的贪心,然后把错误的选择改为正确的。

参考博客:

RioTian的博客
SyadouHayami的博客

反悔堆:

P2949 用时一定的反悔贪心
先按照一般的错误贪心写,不对了再利用小根堆替换。
附上代码:


#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+10;
struct sss{
	int val,date;
	bool operator<(const sss &aaa)const{
		return val>aaa.val;
	}//重载成小根堆
}w[N];
priority_queue<sss> q;
bool cmp(sss x,sss y)
{
    if(x.date!=y.date)return x.date<y.date;
    else return x.val>y.val;
}
ll ans;
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&w[i].date,&w[i].val);
	}
	sort(w+1,w+n+1,cmp);
    for(int i=1;i<=n;i++)
    {
        if(w[i].date>q.size())//可以放进去
        {
            ans+=w[i].val;
            q.push(w[i]);
        }
        else //和前面有冲突,选择与堆顶比较
        {
            if(w[i].val>q.top().val)//较堆顶更为优秀,替换
            {
                ans-=q.top().val;
                q.pop();
                q.push(w[i]);
                ans+=w[i].val;
            }
            //不如堆顶优秀,无视
        }
    }
    printf("%lld",ans);
    return 0;
}

P4053 价值一定的反悔贪心
这题和上面那题没啥区别甚至代码只需要该两三处,这题的“价值”就是 一个物品耗费的time ,只不过我们想要“价值”最小罢了。

附上代码:


#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=150050;
struct sss{
	int time,date;
	bool operator<(const sss &aaa)const{
		return time<aaa.time;
	}//重载成小根堆
}w[N];
priority_queue<sss> q;
bool cmp(sss x,sss y)
{
    if(x.date!=y.date)return x.date<y.date;
    else return x.time<y.time;
}
int ans;
ll last;
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&w[i].time,&w[i].date);
	}
	sort(w+1,w+n+1,cmp);
    for(int i=1;i<=n;i++)
    {
        if(last+w[i].time<w[i].date)//可以放进去
        {
            ans++;
            last+=w[i].time;
//            cout<<last<<"*/*/\n";
            q.push(w[i]);
        }
        else //和前面有冲突,选择与堆顶比较
        {
            if(w[i].time<q.top().time)//较堆顶更为优秀,替换
            {
                ans--;
                last-=q.top().time;
                q.pop();
                q.push(w[i]);
                last+=w[i].time;
                ans++;
            }
            //不如堆顶优秀,无视
        }
    }
    printf("%d",ans);
    return 0;
}

反悔自动机:

CF865D Buy Low Sell High(堆反悔自动机)
对于这题来说,大佬的博客讲得可能太细了……

基础的贪心思想是:

 对于一个点 \(i\) 往后选取一个 \(j\) ,若\(a[j]\)\(a[i]\)大,则选取这一对,\(ans+=a[j]-a[i]\)
 很明显,当后面有一个 \(k\) 点,\(a[k]>a[j]\)时,有可能会让答案更优。
例子:

3
1 2 100

显然应该放弃2,选择卖出100。

考虑后悔:

 我们用一个价值 \(val\) 的物品来帮助我们完成当前的 \(k\) 点与 \(val\) 物品配对后等价于放弃 \(j\) 选择 \(k\) 这个操作,简单推导可得\(val=a[j]\)

 语言是苍白且无力的,看代码:

for(int i=1;i<=n;i++)
{
    if(!q.empty() && (q.top()<a[i]))//堆顶比当前小
    {
        ans+=a[i]-q.top();
        q.pop();
        q.push(a[i]);
    }
    q.push(a[i]);
}

详细代码:



#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=300005;
int a[N];
ll ans;
priority_queue<int ,vector<int>,greater<int> >q;
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    for(int i=1;i<=n;i++)
    {
        if(!q.empty() && (q.top()<a[i]))//堆顶比当前小
        {
            ans+=a[i]-q.top();
            q.pop();
            q.push(a[i]);
        }
        q.push(a[i]);
    }
    printf("%lld",ans);
    return 0;
}

P3620 [APIO/CTSC2007] 数据备份(双向链表反悔自动机)
这个题挺好玩的。
题意
 首先把题目化成为选取k个点,每个点选取后与之相邻都不可被选,最后希望和最小。

错误的贪心想法:

 每次选最小的。
Hack例子:
2 1 2 9
 事实证明选1不如选2 2优。

考虑后悔

 题解讲得挺好还有图看它吧。
image
这道题就没了。
不,还有附上代码:


#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+10;
const int INf=0x3f3f3f3f;
int nxt[N];
int pre[N];
int vis[N];
int dis[N];
int a[N];
struct sss{
	int val,pos;
	sss(int _val=0,int _pos=0)
	{
		val=_val;
		pos=_pos;
	}
	bool operator <(sss aaa) const
	{
		return val>aaa.val;
	} 
};
priority_queue<sss> q;
void change(int x)
{
	vis[pre[x]]=vis[nxt[x]]=true;
	pre[x]=pre[pre[x]];//手玩一下 
	nxt[x]=nxt[nxt[x]];
	nxt[pre[x]]=x;
	pre[nxt[x]]=x;
}
signed main()
{
	int n,k;
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
	}
	for(int i=1;i<n;i++)
	{
		nxt[i]=i-1;
		pre[i]=i+1;
		dis[i]=a[i+1]-a[i];
		q.push(sss(dis[i],i));
	}
	dis[0]=dis[n]=INf;
	int ans=0;
	for(int i=1;i<=k;i++)
	{
		while(vis[q.top().pos])//被删过
		{
			q.pop(); 
		}
		sss now=q.top();
		q.pop();//取出当前最优解,并将其标记删除
		int id=now.pos;
		ans+=dis[id];
		dis[id]=dis[nxt[id]]+dis[pre[id]]-dis[id];
		q.push(sss(dis[id],id));
		change(id);
	}
	printf("%lld",ans);
	return 0;
}

分治

分治,把一个复杂的问题分成多个简单问题,直接求解,最后合并。这个思想是很多高效算法的基础,例如排序算法(快速排序,归并排序),傅里叶变换(快速傅里叶变换)等,但这里不讲。

可以用分治需要满足的性质:

  1. 该问题的规模缩小到一定的程度就可以容易的解决。
  2. 该问题具有最优子结构性质。
  3. 利用该问题分解出的子问题的解可以合并为该问题的解。
  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。

大概的流程可以分为三步:分解 -> 解决 -> 合并。

  1. 分解原问题为结构相同的子问题。
  2. 分解到某个容易求解的边界之后,进行递归求解。
  3. 将子问题的解合并成原问题的解。

Tips:思想是没有上限的。

可以使用分治法求解的一些经典问题:

  1. 二分搜索
  2. 大整数乘法
  3. Strassen矩阵乘法
  4. 棋盘覆盖
  5. 合并排序
  6. 快速排序
  7. 线性时间选择
  8. 最接近点对问题
  9. 循环赛日程表
  10. 汉诺塔

考虑到某些原因,就不细讲了。

放一道好玩的题:

希望不出锅。
P6406 Norma
该题题意很简洁,求\(\Sigma len*min*max\).
思路类似CDQ分治,给定一个区间\([L,R]\)将其划分为\([L,mid]\)\([mid+1,R]\)求解跨过\(mid\)的区间的贡献后,再递归计算\([L,mid]\)\([mid+1,R]\)
考虑固定一个点i,使其范围在\([L,mid]\)中,求出右区间对它的贡献,最后所有\(i \in[L,mid]\)相加求和。
上面只是大题思路,下面细讲一下如何求右边对\(i\)的贡献。

 细想一下,题目求\(\Sigma len*min*max\).我们可以从\(min\)\(max\)入手,先处理出左边区间的最小值和最大值,记为\(minn\)\(maxx\)再处理出\(minn\)\(maxx\)在右边能维持的最大范围。下标分别记做\(j,k\)
 于是乎,有了\(j,k\)两点后,右边被分成了三部分。
为了便于表示,设两点中靠左的点为\(x\),靠右的点为\(y\)
第一部分:\([mid+1,x]\)
 对于这一部分来说,最小值和最大值是前文提到的\(minn\)\(maxx\),是固定的所以可以\(O(1)\)求解。

第二部分:\([x+1,y]\)
 由于\(j\)\(k\)的相对位置的不同会导致不一样的情况,所以需要分类讨论。为了方便,下面仅举\(j<k\)的例子:
\(j<K\)时,意味着区间最小值不是\(minn\),而区间最大值仍然是\(maxx\),那就考虑如何求解右边的区间最小值的贡献。
 为了方便,设当前在右边的指针为\(F\),当前的最小值为\(mi\)
 朴素想法求贡献:

\[\sum_{F=j}^k(F-i+1)*mi*maxx \]

如果真按照它这样求,那计算不用想都知道是不优秀的。
接下来我们对它来稍微改一下后,再运用乘法分配律:

\[\sum_{F=j}^k(F-(mid+1)+1+mid-i+1)*mi*maxx \]

\[\sum_{F=j}^k((F-(mid+1)+1)*mi+(mid-i+1)*mi)*maxx \]

这样我们就可以用两个前缀和来优化啦。

我们可以用前缀和来分成两段处理。
用前缀和分成两段而不能合为一的原因在于式子中有个i,
明显如果不分开这个i的话,就是个暴力了,
所以考虑分开i。

我们使用两个前缀和数组,
其中一个表示 右端点到 mid+1 的 minnlen 和,
由于我们还需要 i 到 mid 的 len
minn 和,
我们就需要另一个前缀和数组 存的是 minn 和(它(i-mid+1) 就是我写的乘法分配律的那个式子 )
两个前缀和相加
maxx(第二个前缀和需要乘(i-mid+1))就是第二段对 i 的贡献。

后言:讲课的时候可能会在黑板上画图,所以就不贴图片啦

附上代码:


#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
const int P=1e9;
const int INf=INT_MAX;
#define ll long long
ll a[N];
ll ans;
ll s1[2][N];
ll s2[2][N];
ll s3[2][N];
ll Gaosi(int a,int b)
{
	return (a+b)*(b-a+1)/2;
}
void Estar(int L,int R)
{
    if(L==R)
    {
        (ans+=a[L]*a[L]%P)%=P;
        return;
    }
    if(L+1==R)
	{
		(ans+=(a[L]*a[L]%P+a[R]*a[R]%P+a[L]*a[R]%P*2%P)%P)%=P;
		return;
	}
    ll mid=(L+R)>>1;
    ll mi=a[mid],mx=a[mid];
    ll j=mid,k=mid;
    ll Mi=a[mid+1],Mx=a[mid+1];
    s1[0][mid]=s2[0][mid]=s3[0][mid]=0;
    s1[1][mid]=s2[1][mid]=s3[1][mid]=0;
    for(ll i=mid+1;i<=R;i++)//预处理出前缀和
    {
        Mi=min(Mi,a[i]);
        Mx=max(Mx,a[i]);
        (s1[0][i]=s1[0][i-1]+(i-mid)*Mi%P)%=P;
        (s2[0][i]=s2[0][i-1]+(i-mid)*Mx%P)%=P;
        (s3[0][i]=s3[0][i-1]+(i-mid)*Mi*Mx%P)%=P;
        (s1[1][i]=s1[1][i-1]+Mi)%=P;
        (s2[1][i]=s2[1][i-1]+Mx)%=P;
        (s3[1][i]=s3[1][i-1]+Mi*Mx%P)%=P;
    }
	ll i=mid;
    while(i>=L)
    {
        mi=min(mi,a[i]);
        mx=max(mx,a[i]);
        while(a[j+1]>mi&&j<R)j++;//注意判断是否越界,或者边界设为INf
        while(a[k+1]<mx&&k<R)k++;
        ll w1=min(j,k);
        ll w2=max(j,k);
        if(w1>mid)(ans+=mi*mx%P*((mid+1-i+1+w1-i+1)*(w1-(mid+1)+1)/2%P)%P)%=P;
        if(k>j)
        {
            (ans+= mx*( (s1[0][k]-s1[0][j]+P)%P + (mid-i+1)*(s1[1][k]-s1[1][j]+P)%P)%P)%=P;
        }
        if(j>k)
            (ans+=mi*((s2[0][j]-s2[0][k]+P)%P+(mid-i+1)*(s2[1][j]-s2[1][k]+P)%P)%P)%=P;
        (ans+= ((s3[0][R]-s3[0][w2]+P)%P + (mid-i+1)*(s3[1][R]-s3[1][w2]+P)%P)%P)%=P;
        i--;
    }
    Estar(L,mid);
    Estar(mid+1,R);
}
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
    }
    Estar(1,n);
    printf("%lld",ans%P);
    return 0;
}

后面都是是一些简单的水题。

前缀和&差分

参考资料:oiwiki

前缀和

 前缀和可以简单理解为「数列的前\(n\)项的和」,是一种重要的预处理方式,能大大降低查询的时间复杂度。

二维/多维前缀和

 一般来讲是运用容斥原理来解决。不一般的比如说基于DP的高位前缀和,有兴趣自己bdfs一下,这里就不讲了 ╮(・o・)╭
放个小题:P1387 最大正方形
 因为是01矩阵,所以可以想二维前缀和表示该区域内1的个数。
题目要求的是最大的全部为1的正方形,那就可以枚举边长l,
判断一个是否有边长为l的正方形满足。
附上代码:


#include<bits/stdc++.h> 
using namespace std;
int a[103][103];
int b[103][103];
int main()
{
	int n,m;
	scanf("%d%d",&n,&m); 
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			scanf("%d",&a[i][j]);
			b[i][j]=b[i][j-1]+b[i-1][j]-b[i-1][j-1]+a[i][j];  // 求前缀和
    	}
	}
	int ans=1;
	int l=2;
	while(l<=min(n, m))
	{
		for(int i=l;i<=n;i++)
		{
    		for(int j=l;j<=m;j++)
			{
        		if(b[i][j]-b[i-l][j]-b[i][j-l]+b[i-l][j-l]==l*l)
				{
        			ans=max(ans,l);
        		}
      		}
    	}
    	l++;
  	}
  	printf("%d",ans);
  	return 0;
}

树上前缀和

image

边权默认赋给了下方节点。

差分

差分可以看做前缀和的逆运算,也就是说对差分取前缀和就可以得到原序列。

差分性质

image
 它可以维护多次对序列的一个区间加上一个数,并在最后询问某一位的数或是多次询问某一位的数。注意:修改操作一定要在查询操作之前。
例子:
使序列\([L,R]\)加上val,则\(b[L]+=k,b[R+1]-=k\)

树上差分

 树上差分可以理解为对树上的某一段路径进行差分操作,这里的路径可以类比一维数组的区间进行理解。例如在对树上的一些路径进行频繁操作,并且询问某条边或者某个点在经过操作后的值的时候,就可以运用树上差分思想了。

 树上差分通常会结合 树基础最近公共祖先 来进行考察。树上差分又分为点差分边差分,在实现上会稍有不同。

点差分

例子:
对树上的一些路径进行访问,问一条路径上的点被访问的次数。
image
前两个式子是在给下面蓝色框中的式子执行加+1操作;
后两个式子是在给红色框中的式子执行加+1操作。
image

边差分

image
把边抽象称点(并用下面的节点来表示“边”这个“抽象点”的信息),结合上面差分的内容显然能推出上面的式子。
例题:
P3128 Max Flow P
我没做

二分&三分

二分

二分查找:

人生建议:建议不要听取别人的人生建议

一般来讲人们喜欢用STL来进行二分查找,但如果你喜欢手写,那code:

int binary_search(int start, int end, int key)
{
	int ret = -1;  // 未搜索到数据返回-1下标
	int mid;
	while (start <= end)
	{
		mid = start + ((end - start) >> 1);  // 直接平均可能会溢出,所以用这个算法
		if (arr[mid] < key)start = mid + 1;
	    else if (arr[mid] > key)end = mid - 1;
    	else  // 最后检测相等是因为多数搜索情况不是大于就是小于
    	{
			ret = mid;
			break;
    	}
	}
	return ret;  // 单一出口
}

如果你喜欢STL,那:

1.binary_search:查找某个元素是否出现。
2.lower_bound:查找第一个大于或等于某个元素的位置。
3.upper_bound:查找第一个大于某个元素的位置.

温馨提示:请保证使用STL时,序列单调。

详解:
1.binary_search:查找某个元素是否出现。

 a.函数模板:binary_search(arr[],arr[]+size , indx)

 b.参数说明:
 arr[]: 数组首地址
 size:数组元素个数
 indx:需要查找的值

 c.函数功能: 在数组中以二分法检索的方式查找,若在数组(要求数组元素非递减)中查找到indx元素则真,若查找不到则返回值为假。

2.lower_bound:查找第一个大于或等于某个元素的位置。
 a.函数模板:lower_bound(arr[],arr[]+size , indx):
 b.参数说明:
 arr[]: 数组首地址
 size:数组元素个数
 indx:需要查找的值
 c.函数功能: 函数lower_bound()在first和last中的前闭后开区间进行二分查找,返回大于或等于val的第一个元素位置(注意是地址)。如果所有元素都小于val,则返回last的位置
 d.举例如下:
  一个数组number序列为:4,10,11,30,69,70,96,100.设要插入数字3,9,111.pos为要插入的位置的下标,则
 /注意因为返回值是一个指针,所以减去数组的指针就是int变量了/
 pos = lower_bound( number, number + 8, 3) - number,pos = 0.即number数组的下标为0的位置。
 pos = lower_bound( number, number + 8, 9) - number, pos = 1,即number数组的下标为1的位置(即10所在的位置)。
 pos = lower_bound( number, number + 8, 111) - number, pos = 8,即number数组的下标为8的位置(但下标上限为7,所以返回最后一个元素的下一个元素)。
 e.注意:函数lower_bound()在first和last中的前闭后开区间进行二分查找,返回大于或等于val的第一个元素位置。如果所有元素都小于val,则返回last的位置,且last的位置是越界的!

 返回查找元素的第一个可安插位置,也就是“元素值>=查找值”的第一个元素的位置

3.upper_bound:查找第一个大于某个元素的位置。
 a.函数模板:upper_bound(arr[],arr[]+size , indx):
 b.参数说明:
 arr[]: 数组首地址
 size:数组元素个数
 indx:需要查找的值
 c.函数功能:函数upper_bound()返回的在前闭后开区间查找的关键字的上界,返回大于val的第一个元素位置
 例如:一个数组number序列1,2,2,4.upper_bound(2)后,返回的位置是3(下标)也就是4所在的位置,同样,如果插入元素大于数组中全部元素,返回的是last。(注意:数组下标越界)
 返回查找元素的最后一个可安插位置,也就是“元素值>查找值”的第一个元素的位置。

搬过来果然还是太丑了,原文链接click here

二分答案

 解题的时候往往会考虑枚举答案然后检验枚举的值是否正确。若满足单调性,则满足使用二分法的条件。把这里的枚举换成二分,就变成了「二分答案」。
 一般来讲,题目中出现“最小值最大,最大值最小”等需要用到二分答案。
 每个人有每个人的二分模板啦,这里就不贴我的了。

三分法

 如果需要求出单峰函数的极值点,通常使用二分法衍生出的三分法求单峰函数的极值点。

 大体思想:左右各取一个点(取\(mid-eps\)\(mid+eps\)是不错的选择),再删去左侧或右侧不符合的区间。
P3382 【模板】三分法
这个题的题解区很乱搞,模拟退火都上场了……
还是,附上代码:


#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=20;
const double eps=1e-6;
ll n;
double l,r,a[N];
double F(double x)
{
    double ans=0;
    for(ll i=n;i>=0;i--) ans=ans*x+a[i];
    return ans;
}
int main()
{
    scanf("%lld%lf%lf",&n,&l,&r);
    for(ll i=n;i>=0;i--)
	scanf("%lf",&a[i]);
    while(r-l>eps)
	{
        double mid=(l+r)/2;
        if(F(mid+eps)>F(mid-eps)) l=mid;
        else r=mid;
    }
    printf("%.5lf",l);
    return 0;
}

倍增

简介:可以优化时间复杂度,常见问题: RMQ 问题和求 LCA(最近公共祖先)。

ST表

ST 表是用于解决 可重复贡献问题的数据结构(数据结构?那貌似我可以不讲?)。

突然发现这和某位要讲的重复了,那就不讲了。

对于RMQ问题的求解
预处理部分:
image

查询部分:
image

LCA

LCA也是倍增的,也重复了,噢耶。
image

构造

嗯?高自由度?
image

image

简介:

 构造题是比赛中常见的一类题型。

 从形式上来看,问题的答案往往具有某种规律性,使得在问题规模迅速增大的时候,仍然有机会比较容易地得到答案。

 这要求解题时要思考问题规模增长对答案的影响,这种影响是否可以推广。例如,在设计动态规划方法的时候,要考虑从一个状态到后继状态的转移会造成什么影响。

特点:

简而言之:看智商看人品

 构造题一个很显著的特点就是高自由度,也就是说一道题的构造方式可能有很多种,但是会有一种较为简单的构造方式满足题意。看起来是放宽了要求,让题目变的简单了,但很多时候,正是这种高自由度导致题目没有明确思路而无从下手。

 构造题另一个特点就是形式灵活,变化多样。并不存在一个通用解法或套路可以解决所有构造题,甚至很难找出解题思路的共性。

来几道好玩的题
P3599 Koishi Loves Construction

官方回答了该题

官方的回答了他的废话

image

这启示我们什么?

大胆想象,大胆猜测,勇夺第一,实创辉煌。

完结,撒花啦✿✿ヽ(°▽°)ノ✿

等等,你以为完了吗?实际上还有一篇

posted @ 2022-11-10 18:53  Estar_Mailyn  阅读(84)  评论(0编辑  收藏  举报