boolcheck(intx){/* ... */}// Vérifie si x satisfait une certaine propriété
// Utilisé lorsque l'intervalle [l, r] est divisé en [l, mid] et [mid + 1, r] :
intbsearch_1(intl,intr){while(l<r){intmid=l+r>>1;if(check(mid))r=mid;// check() vérifie si mid satisfait la propriété
elsel=mid+1;}returnl;}// Utilisé lorsque l'intervalle [l, r] est divisé en [l, mid - 1] et [mid, r] :
intbsearch_2(intl,intr){while(l<r){intmid=l+r+1>>1;if(check(mid))l=mid;elser=mid-1;}returnl;}
2.2 Modèle d’algorithme de recherche binaire pour les nombres à virgule flottante
boolcheck(doublex){/* ... */}// Vérifie si x satisfait une certaine propriété
doublebsearch_3(doublel,doubler){constdoubleeps=1e-6;// eps représente la précision, dépend des exigences de précision du problème
while(r-l>eps){doublemid=(l+r)/2;if(check(mid))r=mid;elsel=mid;}returnl;}
// C = A + B, A >= 0, B >= 0
vector<int>add(vector<int>&A,vector<int>&B){if(A.size()<B.size())returnadd(B,A);vector<int>C;intt=0;for(inti=0;i<A.size();i++){t+=A[i];if(i<B.size())t+=B[i];C.push_back(t%10);t/=10;}if(t)C.push_back(t);returnC;}
// C = A - B, satisfait A >= B, A >= 0, B >= 0
vector<int>sub(vector<int>&A,vector<int>&B){vector<int>C;for(inti=0,t=0;i<A.size();i++){t=A[i]-t;if(i<B.size())t-=B[i];C.push_back((t+10)%10);if(t<0)t=1;elset=0;}while(C.size()>1&&C.back()==0)C.pop_back();returnC;}
3.3 Multiplication de haute précision par basse précision
// C = A * b, A >= 0, b >= 0
vector<int>mul(vector<int>&A,intb){vector<int>C;intt=0;for(inti=0;i<A.size()||t;i++){if(i<A.size())t+=A[i]*b;C.push_back(t%10);t/=10;}while(C.size()>1&&C.back()==0)C.pop_back();returnC;}
3.4 Division de haute précision par basse précision
// A / b = C ... r, A >= 0, b > 0
vector<int>div(vector<int>&A,intb,int&r){vector<int>C;r=0;for(inti=A.size()-1;i>=0;i--){r=r*10+A[i];C.push_back(r/b);r%=b;}reverse(C.begin(),C.end());while(C.size()>1&&C.back()==0)C.pop_back();returnC;}
4 Préfixe et différence
Préfixe : Calculer rapidement la somme d’un segment
Différence : Ajouter rapidement +c à un segment, inverse du préfixe
vector<int>alls;// Stocker toutes les valeurs à discrétiser
sort(alls.begin(),alls.end());// Trier toutes les valeurs
alls.erase(unique(alls.begin(),alls.end()),alls.end());// Supprimer les éléments dupliqués
// Recherche binaire pour trouver la valeur discrétisée de x
intfind(intx)// Trouver la première position supérieure ou égale à x
{intl=0,r=alls.size()-1;while(l<r){intmid=l+r>>1;if(alls[mid]>=x)r=mid;elsel=mid+1;}returnr+1;// Mappé à 1, 2, ...n
}
// Fusionner tous les intervalles qui se chevauchent
voidmerge(vector<PII>&segs){vector<PII>res;sort(segs.begin(),segs.end());intst=-2e9,ed=-2e9;for(autoseg:segs)if(ed<seg.first){if(st!=-2e9)res.push_back({st,ed});st=seg.first,ed=seg.second;}elseed=max(ed,seg.second);if(st!=-2e9)res.push_back({st,ed});segs=res;}
Structures de données
1 Listes chaînées et listes d’adjacence : stockage d’arbres et de graphes
// head stocke la tête de la liste chaînée, e[] stocke les valeurs des nœuds, ne[] stocke les pointeurs next des nœuds, idx indique quel nœud est actuellement utilisé
inthead,e[N],ne[N],idx;// Initialisation
voidinit(){head=-1;idx=0;}// Insérer un nombre a en tête de la liste chaînée
voidinsert(inta){e[idx]=a,ne[idx]=head,head=idx++;}// Supprimer le nœud de tête, il faut s'assurer que le nœud de tête existe
voidremove(){head=ne[head];}
// e[] représente la valeur des nœuds, l[] représente le pointeur gauche des nœuds, r[] représente le pointeur droit des nœuds, idx indique quel nœud est actuellement utilisé
inte[N],l[N],r[N],idx;// Initialisation
voidinit(){//0 est le point d'extrémité gauche, 1 est le point d'extrémité droit
r[0]=1,l[1]=0;idx=2;}// Insérer un nombre x à droite du nœud a
voidinsert(inta,intx){e[idx]=x;l[idx]=a,r[idx]=r[a];l[r[a]]=idx,r[a]=idx++;}// Supprimer le nœud a
voidremove(inta){l[r[a]]=l[a];r[l[a]]=r[a];}
// tt représente le sommet de la pile
intstk[N],tt=0;// Insérer un nombre au sommet de la pile
stk[++tt]=x;// Retirer un nombre du sommet de la pile
tt--;// Valeur au sommet de la pile
stk[tt];// Vérifier si la pile est vide, si tt > 0, alors elle n'est pas vide
if(tt>0){}
// hh représente la tête de la file d'attente, tt représente la queue de la file d'attente
intq[N],hh=0,tt=-1;// Insérer un nombre à la queue de la file d'attente
q[++tt]=x;// Retirer un nombre de la tête de la file d'attente
hh++;// Valeur à la tête de la file d'attente
q[hh];// Vérifier si la file d'attente est vide, si hh <= tt, alors elle n'est pas vide
if(hh<=tt){}
File d’attente circulaire
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// hh représente la tête de la file d'attente, tt représente la position après la queue de la file d'attente
intq[N],hh=0,tt=0;// Insérer un nombre à la queue de la file d'attente
q[tt++]=x;if(tt==N)tt=0;// Retirer un nombre de la tête de la file d'attente
hh++;if(hh==N)hh=0;// Valeur à la tête de la file d'attente
q[hh];// Vérifier si la file d'attente est vide, si hh != tt, alors elle n'est pas vide
if(hh!=tt){}
2.3 Pile monotone
—— Problème modèle AcWing 830. Pile monotone
Modèle courant : trouver pour chaque nombre le plus proche à gauche qui est plus grand/petit que lui
—— Problème modèle AcWing 154. Fenêtre coulissante
Modèle courant : trouver la valeur maximale/minimale dans une fenêtre coulissante
1
2
3
4
5
6
7
inthh=0,tt=-1;for(inti=0;i<n;i++){while(hh<=tt&&check_out(q[hh]))hh++;// Vérifier si la tête de file est hors de la fenêtre
while(hh<=tt&&check(q[tt],i))tt--;q[++tt]=i;}
// s[] est le texte long, p[] est la chaîne modèle, n est la longueur de s, m est la longueur de p
CalculerletableauNextdelachaînemodèle:for(inti=2,j=0;i<=m;i++){while(j&&p[i]!=p[j+1])j=ne[j];if(p[i]==p[j+1])j++;ne[i]=j;}// Appariement
for(inti=1,j=0;i<=n;i++){while(j&&s[i]!=p[j+1])j=ne[j];if(s[i]==p[j+1])j++;if(j==m){j=ne[j];// Logique après un appariement réussi
}}// La fonction de bibliothèque C strstr() est ainsi
intson[N][26],cnt[N],idx;// Le point 0 est à la fois le nœud racine et le nœud vide
// son[][] stocke les fils de chaque nœud de l'arbre
// cnt[] stocke le nombre de mots se terminant à chaque nœud
// Insérer une chaîne
voidinsert(char*str){intp=0;for(inti=0;str[i];i++){intu=str[i]-'a';if(!son[p][u])son[p][u]=++idx;p=son[p][u];}cnt[p]++;}// Rechercher le nombre d'occurrences d'une chaîne
intquery(char*str){intp=0;for(inti=0;str[i];i++){intu=str[i]-'a';if(!son[p][u])return0;p=son[p][u];}returncnt[p];}// On peut utiliser map<string,int>, mais le temps est plus de deux fois supérieur
intp[N];// Stocke le nœud ancêtre de chaque point
// Retourne le nœud ancêtre de x
intfind(intx){if(p[x]!=x)p[x]=find(p[x]);returnp[x];}// Initialisation, supposons que les numéros de nœud sont de 1 à n
for(inti=1;i<=n;i++)p[i]=i;// Fusionner les deux ensembles contenant a et b :
p[find(a)]=find(b);
intp[N],size[N];//p[] stocke le nœud ancêtre de chaque point, size[] n'a de sens que pour les nœuds ancêtres, indique le nombre de points dans l'ensemble du nœud ancêtre
// Retourne le nœud ancêtre de x
intfind(intx){if(p[x]!=x)p[x]=find(p[x]);returnp[x];}// Initialisation, supposons que les numéros de nœud sont de 1 à n
for(inti=1;i<=n;i++){p[i]=i;size[i]=1;}// Fusionner les deux ensembles contenant a et b :
size[find(b)]+=size[find(a)];p[find(a)]=find(b);
(3) Ensemble disjoint maintenant la distance au nœud ancêtre :
intp[N],d[N];//p[] stocke le nœud ancêtre de chaque point, d[x] stocke la distance de x à p[x]
// Retourne le nœud ancêtre de x
intfind(intx){if(p[x]!=x){intu=find(p[x]);d[x]+=d[p[x]];p[x]=u;}returnp[x];}// Initialisation, supposons que les numéros de nœud sont de 1 à n
for(inti=1;i<=n;i++){p[i]=i;d[i]=0;}// Fusionner les deux ensembles contenant a et b :
p[find(a)]=find(b);d[find(a)]=distance;// Selon le problème spécifique, initialiser le décalage de find(a)
// h[N] stocke les valeurs dans le tas, h[1] est le sommet du tas, le fils gauche de x est 2x, le fils droit est 2x + 1
// ph[k] stocke la position dans le tas du k-ième point inséré
// hp[k] stocke le k-ième point dans le tas est le k-ième inséré
inth[N],ph[N],hp[N],size;// Échanger deux points et leur relation de mappage
voidheap_swap(inta,intb){swap(ph[hp[a]],ph[hp[b]]);swap(hp[a],hp[b]);swap(h[a],h[b]);}voiddown(intu){intt=u;if(u*2<=size&&h[u*2]<h[t])t=u*2;if(u*2+1<=size&&h[u*2+1]<h[t])t=u*2+1;if(u!=t){heap_swap(u,t);down(t);}}voidup(intu){while(u/2&&h[u]<h[u/2]){heap_swap(u,u/2);u>>=1;}}// O(n) construction de tas
for(inti=n/2;i;i--)down(i);
(1)Méthodedeschaînesinth[N],e[N],ne[N],idx;// Insérer un nombre dans la table de hachage
voidinsert(intx){intk=(x%N+N)%N;e[idx]=x;ne[idx]=h[k];h[k]=idx++;}// Rechercher si un nombre existe dans la table de hachage
boolfind(intx){intk=(x%N+N)%N;for(inti=h[k];i!=-1;i=ne[i])if(e[i]==x)returntrue;returnfalse;}(2)Méthodederechercheouverteinth[N];// Si x est dans la table de hachage, retourne l'indice de x ; si x n'est pas dans la table de hachage, retourne la position où x devrait être inséré
intfind(intx){intt=(x%N+N)%N;while(h[t]!=null&&h[t]!=x){t++;if(t==N)t=0;}returnt;}
7.2 Hachage de chaînes
—— Problème modèle AcWing 841. Hachage de chaînes
Idée principale : considérer la chaîne comme un nombre en base P, la valeur expérimentale de P est 131 ou 13331, la probabilité de collision avec ces deux valeurs est faible
Petite astuce : utiliser 2^64 comme nombre de modulo, ainsi on peut directement utiliser unsigned long long pour stocker, le résultat du débordement est le résultat du modulo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedefunsignedlonglongULL;ULLh[N],p[N];// h[k] stocke la valeur de hachage des k premiers caractères de la chaîne, p[k] stocke P^k mod 2^64
// Initialisation
p[0]=1;for(inti=1;i<=n;i++){h[i]=h[i-1]*P+str[i];p[i]=p[i-1]*P;}// Calculer la valeur de hachage de la sous-chaîne str[l ~ r]
ULLget(intl,intr){returnh[r]-h[l-1]*p[r-l+1];}
Un arbre est un type spécial de graphe, et sa méthode de stockage est la même que celle des graphes.
Pour une arête ab dans un graphe non orienté, stocker deux arêtes orientées a->b, b->a.
Ainsi, nous pouvons seulement considérer le stockage des graphes orientés.
(1) Matrice d’adjacence : g[a][b] stocke l’arête a->b
(2) Liste d’adjacence :
1
2
3
4
5
6
7
8
9
10
11
12
// Pour chaque point k, ouvrir une liste chaînée simple, stocker tous les points atteignables à partir de k. h[k] stocke le nœud de tête de cette liste chaînée
inth[N],e[N],ne[N],idx;// Ajouter une arête a->b
voidadd(inta,intb){e[idx]=b,ne[idx]=h[a],h[a]=idx++;}// Initialisation
idx=0;memset(h,-1,sizeofh);
1.2 Parcours d’arbres et de graphes
Complexité temporelle O(n+m), n représente le nombre de points, m représente le nombre d’arêtes
(1) Parcours en profondeur —— Problème modèle AcWing 846. Centre d’un arbre
1
2
3
4
5
6
7
8
9
10
intdfs(intu){st[u]=true;// st[u] représente que le point u a déjà été parcouru
for(inti=h[u];i!=-1;i=ne[i]){intj=e[i];if(!st[j])dfs(j);}}
queue<int>q;st[1]=true;// Représente que le point 1 a déjà été parcouru
q.push(1);while(q.size()){intt=q.front();q.pop();for(inti=h[t];i!=-1;i=ne[i]){intj=e[i];if(!st[j]){st[j]=true;// Représente que le point j a déjà été parcouru
q.push(j);}}}
booltopsort(){inthh=0,tt=-1;// d[i] stocke le degré entrant du point i
for(inti=1;i<=n;i++)if(!d[i])q[++tt]=i;while(hh<=tt){intt=q[hh++];for(inti=h[t];i!=-1;i=ne[i]){intj=e[i];if(--d[j]==0)q[++tt]=j;}}// Si tous les points ont été ajoutés à la file, cela signifie qu'il existe une séquence topologique ; sinon, il n'en existe pas.
returntt==n-1;}
intg[N][N];// Stocke chaque arête
intdist[N];// Stocke la distance la plus courte du point 1 à chaque point
boolst[N];// Stocke si la distance la plus courte de chaque point a déjà été déterminée
// Calculer la distance la plus courte du point 1 au point n, si elle n'existe pas, retourner -1
intdijkstra(){memset(dist,0x3f,sizeofdist);dist[1]=0;for(inti=0;i<n-1;i++){intt=-1;// Parmi les points dont la distance la plus courte n'est pas encore déterminée, trouver celui avec la plus petite distance
for(intj=1;j<=n;j++)if(!st[j]&&(t==-1||dist[t]>dist[j]))t=j;// Utiliser t pour mettre à jour la distance des autres points
for(intj=1;j<=n;j++)dist[j]=min(dist[j],dist[t]+g[t][j]);st[t]=true;}if(dist[n]==0x3f3f3f3f)return-1;returndist[n];}
typedefpair<int,int>PII;intn;// Nombre de points
inth[N],w[N],e[N],ne[N],idx;// Liste d'adjacence stockant toutes les arêtes
intdist[N];// Stocke la distance de tous les points au point 1
boolst[N];// Stocke si la distance la plus courte de chaque point a déjà été déterminée
// Calculer la distance la plus courte du point 1 au point n, si elle n'existe pas, retourner -1
intdijkstra(){memset(dist,0x3f,sizeofdist);dist[1]=0;priority_queue<PII,vector<PII>,greater<PII>>heap;heap.push({0,1});// first stocke la distance, second stocke le numéro du point
while(heap.size()){autot=heap.top();heap.pop();intver=t.second,distance=t.first;if(st[ver])continue;st[ver]=true;for(inti=h[ver];i!=-1;i=ne[i]){intj=e[i];if(dist[j]>distance+w[i]){dist[j]=distance+w[i];heap.push({dist[j],j});}}}if(dist[n]==0x3f3f3f3f)return-1;returndist[n];}
3.3 Algorithme de Bellman-Ford
—— Problème modèle AcWing 853. Chemin le plus court avec limite sur le nombre d’arêtes
Complexité temporelle O(nm), n représente le nombre de points, m représente le nombre d’arêtes
Attention, dans le problème modèle, il est nécessaire de modifier légèrement le modèle ci-dessous, en ajoutant un tableau de sauvegarde, voir le problème modèle pour plus de détails.
intn,m;// n représente le nombre de points, m représente le nombre d'arêtes
intdist[N];// dist[x] stocke la distance la plus courte de 1 à x
structEdge// Arête, a représente le point de départ, b représente le point d'arrivée, w représente le poids de l'arête
{inta,b,w;}edges[M];// Calculer la distance la plus courte de 1 à n, si elle n'existe pas, retourner -1.
intbellman_ford(){memset(dist,0x3f,sizeofdist);dist[1]=0;// Si la n-ième itération continue de relâcher l'inégalité triangulaire, cela signifie qu'il existe un chemin de longueur n+1, selon le principe des tiroirs, il y a au moins deux points identiques sur le chemin, indiquant qu'il existe un cycle de poids négatif dans le graphe.
for(inti=0;i<n;i++){for(intj=0;j<m;j++){inta=edges[j].a,b=edges[j].b,w=edges[j].w;if(dist[b]>dist[a]+w)dist[b]=dist[a]+w;}}if(dist[n]>0x3f3f3f3f/2)return-1;returndist[n];}
3.4 Algorithme spfa (version optimisée par file d’attente de Bellman-Ford)
intn;// Nombre total de points
inth[N],w[N],e[N],ne[N],idx;// Liste d'adjacence stockant toutes les arêtes
intdist[N];// Stocke la distance la plus courte de chaque point au point 1
boolst[N];// Stocke si chaque point est dans la file d'attente
// Calculer la distance la plus courte du point 1 au point n, si elle n'existe pas, retourner -1
intspfa(){memset(dist,0x3f,sizeofdist);dist[1]=0;queue<int>q;q.push(1);st[1]=true;while(q.size()){autot=q.front();q.pop();st[t]=false;for(inti=h[t];i!=-1;i=ne[i]){intj=e[i];if(dist[j]>dist[t]+w[i]){dist[j]=dist[t]+w[i];if(!st[j])// Si j est déjà dans la file d'attente, ne pas l'insérer à nouveau
{q.push(j);st[j]=true;}}}}if(dist[n]==0x3f3f3f3f)return-1;returndist[n];}
3.5 spfa pour déterminer s’il existe un cycle négatif dans le graphe
intn;// Nombre total de points
inth[N],w[N],e[N],ne[N],idx;// Liste d'adjacence stockant toutes les arêtes
intdist[N],cnt[N];// dist[x] stocke la distance la plus courte de 1 à x, cnt[x] stocke le nombre de points traversés dans le chemin le plus court de 1 à x
boolst[N];// Stocke si chaque point est dans la file d'attente
// Si un cycle négatif existe, retourner true, sinon retourner false.
boolspfa(){// Pas besoin d'initialiser le tableau dist
// Principe : si un chemin le plus court contient n points (à l'exception de lui-même), alors en ajoutant lui-même, il y a n+1 points, selon le principe des tiroirs, il y a au moins deux points identiques, donc il existe un cycle.
queue<int>q;for(inti=1;i<=n;i++){q.push(i);st[i]=true;}while(q.size()){autot=q.front();q.pop();st[t]=false;for(inti=h[t];i!=-1;i=ne[i]){intj=e[i];if(dist[j]>dist[t]+w[i]){dist[j]=dist[t]+w[i];cnt[j]=cnt[t]+1;if(cnt[j]>=n)returntrue;// Si le chemin le plus court de 1 à x contient au moins n points (à l'exception de lui-même), cela signifie qu'il existe un cycle
if(!st[j]){q.push(j);st[j]=true;}}}}returnfalse;}
Initialisation:for(inti=1;i<=n;i++)for(intj=1;j<=n;j++)if(i==j)d[i][j]=0;elsed[i][j]=INF;// Après l'algorithme, d[a][b] représente la distance la plus courte de a à b
voidfloyd(){for(intk=1;k<=n;k++)for(inti=1;i<=n;i++)for(intj=1;j<=n;j++)d[i][j]=min(d[i][j],d[i][k]+d[k][j]);}
intn;// n représente le nombre de points
intg[N][N];// Matrice d'adjacence, stocke toutes les arêtes
intdist[N];// Stocke la distance des autres points à l'arbre couvrant minimal actuel
boolst[N];// Stocke si chaque point est déjà dans l'arbre couvrant minimal
// Si le graphe n'est pas connexe, retourner INF (valeur 0x3f3f3f3f), sinon retourner la somme des poids des arêtes de l'arbre couvrant minimal
intprim(){memset(dist,0x3f,sizeofdist);intres=0;for(inti=0;i<n;i++){intt=-1;for(intj=1;j<=n;j++)if(!st[j]&&(t==-1||dist[t]>dist[j]))t=j;if(i&&dist[t]==INF)returnINF;if(i)res+=dist[t];st[t]=true;for(intj=1;j<=n;j++)dist[j]=min(dist[j],g[t][j]);}returnres;}
intn,m;// n est le nombre de points, m est le nombre d'arêtes
intp[N];// Tableau des nœuds parents de l'ensemble disjoint
structEdge// Stocke les arêtes
{inta,b,w;booloperator<(constEdge&W)const{returnw<W.w;}}edges[M];intfind(intx)// Opération principale de l'ensemble disjoint
{if(p[x]!=x)p[x]=find(p[x]);returnp[x];}intkruskal(){sort(edges,edges+m);for(inti=1;i<=n;i++)p[i]=i;// Initialisation de l'ensemble disjoint
intres=0,cnt=0;for(inti=0;i<m;i++){inta=edges[i].a,b=edges[i].b,w=edges[i].w;a=find(a),b=find(b);if(a!=b)// Si deux blocs connexes ne sont pas connectés, les fusionner
{p[a]=b;res+=w;cnt++;}}if(cnt<n-1)returnINF;returnres;}
5 Graphe biparti
5.1 Méthode de coloration pour déterminer un graphe biparti
intn;// n représente le nombre de points
inth[N],e[M],ne[M],idx;// Liste d'adjacence stockant le graphe
intcolor[N];// Représente la couleur de chaque point, -1 signifie non coloré, 0 signifie blanc, 1 signifie noir
// Paramètres : u représente le nœud actuel, c représente la couleur actuelle du point
booldfs(intu,intc){color[u]=c;for(inti=h[u];i!=-1;i=ne[i]){intj=e[i];if(color[j]==-1){if(!dfs(j,!c))returnfalse;}elseif(color[j]==c)returnfalse;}returntrue;}boolcheck(){memset(color,-1,sizeofcolor);boolflag=true;for(inti=1;i<=n;i++)if(color[i]==-1)if(!dfs(i,0)){flag=false;break;}returnflag;}
intn1,n2;// n1 représente le nombre de points dans le premier ensemble, n2 représente le nombre de points dans le deuxième ensemble
inth[N],e[M],ne[M],idx;// Liste d'adjacence stockant toutes les arêtes, l'algorithme hongrois n'utilisera que les arêtes allant du premier ensemble au deuxième ensemble, donc il suffit de stocker un seul sens d'arête
intmatch[N];// Stocke le point du premier ensemble actuellement apparié à chaque point du deuxième ensemble
boolst[N];// Représente si chaque point du deuxième ensemble a déjà été parcouru
boolfind(intx){for(inti=h[x];i!=-1;i=ne[i]){intj=e[i];if(!st[j]){st[j]=true;if(match[j]==0||find(match[j])){match[j]=x;returntrue;}}}returnfalse;}// Calculer le nombre maximal d'appariements, énumérer successivement si chaque point du premier ensemble peut être apparié à un point du deuxième ensemble
intres=0;for(inti=1;i<=n1;i++){memset(st,false,sizeofst);if(find(i))res++;}
Connaissances mathématiques
1 Nombres premiers
1.1 Méthode de division pour déterminer si un nombre est premier
intprimes[N],cnt;// primes[] stocke tous les nombres premiers
boolst[N];// st[x] stocke si x a été criblé
voidget_primes(intn){for(inti=2;i<=n;i++){if(st[i])continue;primes[cnt++]=i;for(intj=i+i;j<=n;j+=i)st[j]=true;}}
1.4 Crible linéaire pour trouver les nombres premiers
intprimes[N],cnt;// primes[] stocke tous les nombres premiers
boolst[N];// st[x] stocke si x a été criblé
voidget_primes(intn){for(inti=2;i<=n;i++){if(!st[i])primes[cnt++]=i;for(intj=0;primes[j]<=n/i;j++){st[primes[j]*i]=true;if(i%primes[j]==0)break;}}}
2 Diviseurs
2.1 Méthode de division pour trouver tous les diviseurs
intprimes[N],cnt;// primes[] stocke tous les nombres premiers
inteuler[N];// Stocke la fonction d'Euler de chaque nombre
boolst[N];// st[x] stocke si x a été criblé
voidget_eulers(intn){euler[1]=1;for(inti=2;i<=n;i++){if(!st[i]){primes[cnt++]=i;euler[i]=i-1;}for(intj=0;primes[j]<=n/i;j++){intt=primes[j]*i;st[t]=true;if(i%primes[j]==0){euler[t]=euler[i]*primes[j];break;}euler[t]=euler[i]*(primes[j]-1);}}}
// a[N][N] est la matrice augmentée
intgauss(){intc,r;for(c=0,r=0;c<n;c++){intt=r;for(inti=r;i<n;i++)// Trouver la ligne avec la plus grande valeur absolue
if(fabs(a[i][c])>fabs(a[t][c]))t=i;if(fabs(a[t][c])<eps)continue;for(inti=c;i<=n;i++)swap(a[t][i],a[r][i]);// Échanger la ligne avec la plus grande valeur absolue avec la ligne du haut
for(inti=n;i>=c;i--)a[r][i]/=a[r][c];// Transformer le premier élément de la ligne actuelle en 1
for(inti=r+1;i<n;i++)// Utiliser la ligne actuelle pour transformer toutes les colonnes en dessous en 0
if(fabs(a[i][c])>eps)for(intj=n;j>=c;j--)a[i][j]-=a[r][j]*a[i][c];r++;}if(r<n){for(inti=r;i<n;i++)if(fabs(a[i][n])>eps)return2;// Pas de solution
return1;// Il y a un nombre infini de solutions
}for(inti=n-1;i>=0;i--)for(intj=i+1;j<n;j++)a[i][n]-=a[i][j]*a[j][n];return0;// Il y a une solution unique
}
7 Comptage combinatoire
7.1 Méthode de récurrence pour calculer les combinaisons
// c[a][b] représente le nombre de façons de choisir b pommes parmi a
for(inti=0;i<N;i++)for(intj=0;j<=i;j++)if(!j)c[i][j]=1;elsec[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
7.2 Calculer les combinaisons en prétraitant les inverses
—— Problème modèle AcWing 886. Calculer les combinaisons II
Tout d’abord, prétraiter tous les restes de factorielle fact[N], ainsi que tous les restes d’inverse de factorielle infact[N]
Si le nombre de modulo est un nombre premier, on peut utiliser le petit théorème de Fermat pour calculer l’inverse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
intqmi(inta,intk,intp)// Modèle de puissance rapide
{intres=1;while(k){if(k&1)res=(LL)res*a%p;a=(LL)a*a%p;k>>=1;}returnres;}// Prétraiter les restes de factorielle et les restes d'inverse de factorielle
fact[0]=infact[0]=1;for(inti=1;i<N;i++){fact[i]=(LL)fact[i-1]*i%mod;infact[i]=(LL)infact[i-1]*qmi(i,mod-2,mod)%mod;}
Sipestunnombrepremier,alorspourtoutentier1<=m<=n,ona:C(n,m)=C(n%p,m%p)*C(n/p,m/p)(modp)intqmi(inta,intk,intp)// Modèle de puissance rapide
{intres=1%p;while(k){if(k&1)res=(LL)res*a%p;a=(LL)a*a%p;k>>=1;}returnres;}intC(inta,intb,intp)// Calculer les combinaisons C(a, b) à l'aide du théorème
{if(a<b)return0;LLx=1,y=1;// x est le numérateur, y est le dénominateur
for(inti=a,j=1;j<=b;i--,j++){x=(LL)x*i%p;y=(LL)y*j%p;}returnx*(LL)qmi(y,p-2,p)%p;}intlucas(LLa,LLb,intp){if(a<p&&b<p)returnC(a,b,p);return(LL)C(a%p,b%p,p)*lucas(a/p,b/p,p)%p;}
7.4 Calculer les combinaisons en décomposant en facteurs premiers
—— Problème modèle AcWing 888. Calculer les combinaisons IV
Lorsque nous devons calculer la valeur réelle des combinaisons, et non le reste par rapport à un certain nombre, la méthode de décomposition en facteurs premiers est plus utile :
1. Crible pour trouver tous les nombres premiers dans la plage
2. Calculer le nombre de fois que chaque facteur premier apparaît à l’aide de C(a, b) = a! / b! / (a - b)! . Le nombre de fois que p apparaît dans n! est n / p + n / p^2 + n / p^3 + …
3. Utiliser la multiplication de haute précision pour multiplier tous les facteurs premiers
intprimes[N],cnt;// Stocke tous les nombres premiers
intsum[N];// Stocke le nombre de fois que chaque nombre premier apparaît
boolst[N];// Stocke si chaque nombre a été criblé
voidget_primes(intn)// Crible linéaire pour trouver les nombres premiers
{for(inti=2;i<=n;i++){if(!st[i])primes[cnt++]=i;for(intj=0;primes[j]<=n/i;j++){st[primes[j]*i]=true;if(i%primes[j]==0)break;}}}intget(intn,intp)// Calculer le nombre de fois que p apparaît dans n!
{intres=0;while(n){res+=n/p;n/=p;}returnres;}vector<int>mul(vector<int>a,intb)// Modèle de multiplication de haute précision par basse précision
{vector<int>c;intt=0;for(inti=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;}returnc;}get_primes(a);// Prétraiter tous les nombres premiers dans la plage
for(inti=0;i<cnt;i++)// Calculer le nombre de fois que chaque facteur premier apparaît
{intp=primes[i];sum[i]=get(a,p)-get(b,p)-get(a-b,p);}vector<int>res;res.push_back(1);for(inti=0;i<cnt;i++)// Utiliser la multiplication de haute précision pour multiplier tous les facteurs premiers
for(intj=0;j<sum[i];j++)res=mul(res,primes[i]);
8 Théorie des jeux
8.1 Nombres de Catalan
—— Problème modèle AcWing 889. Séquence 01 satisfaisant les conditions
Étant donné n zéros et n uns, ils sont disposés dans un ordre quelconque pour former une séquence de longueur 2n, satisfaisant que dans tout préfixe, le nombre de zéros n’est pas inférieur au nombre d’uns. Le nombre de telles séquences est : Cat(n) = C(2n, n) / (n + 1)
Jeu de NIM —— Problème modèle AcWing 891. Jeu de Nim
Étant donné N tas d’objets, la i-ème tas contient Ai objets. Deux joueurs jouent à tour de rôle, chaque fois, ils peuvent choisir une tas et retirer un nombre quelconque d’objets, ils peuvent retirer tous les objets d’une tas, mais ne peuvent pas ne rien retirer. Le joueur qui retire le dernier objet gagne. Les deux joueurs adoptent une stratégie optimale, le premier joueur est-il assuré de gagner ?
Nous appelons ce jeu un jeu de NIM. L’état auquel le jeu est confronté pendant le jeu est appelé une position. Le joueur qui effectue la première action dans le jeu entier est appelé le premier joueur, et le joueur qui effectue la deuxième action est appelé le deuxième joueur. Si dans une position donnée, quelle que soit l’action entreprise, le joueur perdra le jeu, alors cette position est appelée une position perdante.
Adopter une stratégie optimale signifie que si dans une position donnée, il existe une action qui, après l’avoir entreprise, met l’adversaire dans une position perdante, alors cette action est prioritaire. Dans le même temps, une telle position est appelée une position gagnante. Les problèmes de théorie des jeux que nous discutons ne considèrent généralement que la situation idéale, c’est-à-dire que les deux joueurs ne commettent pas d’erreurs et adoptent tous deux une stratégie optimale lors de l’action.
Le jeu de NIM n’a pas de match nul, il n’y a que deux situations : le premier joueur gagne ou le premier joueur perd.
Théorème : Le premier joueur du jeu de NIM gagne si et seulement si A1 ^ A2 ^ … ^ An != 0
8.2 Jeu de combinaison équitable ICG
Si un jeu satisfait :
Deux joueurs agissent à tour de rôle ;
À tout moment du jeu, les actions légales possibles ne dépendent pas de quel joueur doit agir ;
Le joueur qui ne peut pas agir perd ;
alors ce jeu est appelé un jeu de combinaison équitable.
Le jeu de NIM appartient aux jeux de combinaison équitables, mais les jeux de construction, comme le go, ne sont pas des jeux de combinaison équitables. Parce que dans le go, les deux joueurs ne peuvent poser que des pierres noires et blanches respectivement, et le