Question

Imgur Album avec des écrans de feuilles de calcul : http://imgur.com/a/6rFWF

Pour faire court, j'écris un utilitaire Excel VBA qui attribuera deux types d'équipes de sécurité (appelées couvertures et tâches de week-end) aux membres du personnel de sécurité.Fondamentalement, j'ai une feuille de travail avec tous les membres du personnel et leurs diverses informations de disponibilité (l'image du haut dans l'album imgur) et une feuille de travail avec toutes les dates de couverture (l'image du bas dans l'album imgur).Notez que je n'ai pas d'image des dates de service du week-end car elles ressemblent aux dates de couverture (mais avec les équipes du vendredi et du samedi).

L'utilitaire attribue essentiellement un membre du personnel aléatoire à chaque date, vérifiant que cela ne viole aucune de leurs exigences de disponibilité.Malheureusement, je me rends compte que je crée une grande chance qu'une boucle infinie se produise.Lors de mes propres tests, il n'y a eu qu'une seule tentative sur environ 15-16 qui n'est pas entrée dans une boucle infinie vers la fin.Je recherche donc votre aide pour en tenir compte afin que l'utilitaire ne se mange pas tout seul.

Voici le "pseudo-code" de la procédure en question.

'Loop for Column A in the Coverage Slips sheet (image 2 in imgur album)
Do Until (CoverageRowNumber = LastCoverageSlipRow + 1)
    Get a Random Staff Member by RNG
    If staff member still needs more shifts (see Requirements columns) Then
        If staff member does not have an "X" under the day of the week Then
            If staff member does not have a matching date conflict Then
                Assign the coverage
                Increase CoverageRowNumber
            End If
        End If
     End If
Loop

'Loop for Column B in the coverage slips sheet (image 2 in imgur album)
Do Until...

    Same as the loop above

Loop

Modifier:Ne tenez pas compte du fait que j'ai les dates sur deux colonnes pour l'instant.Je vais résoudre ce problème une fois que j'aurai résolu le problème de cet article... c'est une solution simple et réduira le code presque de moitié.

Le problème est que, à mesure que le service public approche de la fin de la liste des dates, il se retrouve souvent dans un scénario dans lequel les seuls membres du personnel restants ne peuvent pas assurer ce quart de travail spécifique (que ce soit en raison du jour de la semaine ou d'une date spécifique).Dans le cas où cela se produirait dans ce scénario, je peux voir quelques options acceptables (même si je ne sais pas comment je procéderais pour les programmer) :

  1. Annulez tout le travail effectué par l'utilitaire et recommencez jusqu'à ce qu'il ait de la chance et trouve une solution qui fonctionne.Cela me ferait gagner du temps lors des placements manuels pour les dernières équipes, mais cela pourrait prendre beaucoup de temps.De plus, je devrais stocker toutes les valeurs d'origine, puis les recoller dans la feuille de calcul à chaque fois qu'elle recommence.

  2. Arrêtez simplement d'attribuer des équipes et quittez simplement la procédure.Je pourrai placer manuellement les derniers quarts de travail en déplaçant quelques personnes.Je suis sûr que cela représente beaucoup moins de travail que d'attribuer manuellement 200 équipes comme je l'ai fait ces dernières années.

Avez-vous des idées qui pourraient être utiles ici ?Je ne sais même pas comment je pourrais faire vérifier la procédure pour voir s'il existe des options disponibles ou non, mais dans tous les cas, il doit y avoir un moyen de détecter (et de dissuader) cette boucle infinie avant qu'elle ne plante le programme.

Désolé pour le roman et merci d'avance pour toute aide !

Modifier:Dans un effort pour fournir un peu plus de clarté, j'ai pensé copier et coller le code ci-dessous :

'------------------------------------------------------------'
'Create ws variables for each worksheet
Dim wsConflicts As Worksheet
Dim wsCoverageSlips As Worksheet
Dim wsWDSlips As Worksheet
Dim wsCoverageOutput As Worksheet
Dim wsWDOutput As Worksheet

'------------------------------------------------------------'
Public Function SetSheets()
'Assign the worksheets to the ws variables
    Set wsConflicts = Worksheets("Conflicts")
    Set wsCoverageSlips = Worksheets("Coverage Slips")
    Set wsWDSlips = Worksheets("WD Slips")
    Set wsCoverageOutput = Worksheets("Coverage Output")
    Set wsWDOutput = Worksheets("WD Output")

'Display a message (debugging)
    'MsgBox "The sheets have been assigned successfully"

End Function

'------------------------------------------------------------'
Public Function ColumnLetter(ColumnNumber As Integer) As String
    Dim n As Long
    Dim c As Byte
    Dim s As String

    n = ColumnNumber
    Do
        c = ((n - 1) Mod 26)
        s = Chr(c + 65) & s
        n = (n - c) \ 26
    Loop While n > 0
    ColumnLetter = s
End Function

'------------------------------------------------------------'
Sub AssignCoverages()
'Fill the ws variables
    Call SetSheets

'Set the first and last row numbers
    Dim FirstStaffMemberRow As Integer
        FirstStaffMemberRow = 3
    Dim LastStaffMemberRow As Integer
        LastStaffMemberRow = wsConflicts.UsedRange.Rows.Count

'Count the number of required coverages and weekend duties
    Dim RequiredCoverages As Integer
    Dim RequiredWDs As Integer
    For i = FirstStaffMemberRow To LastStaffMemberRow
        RequiredCoverages = RequiredCoverages + wsConflicts.Range("B" & i).Value
        RequiredWDs = RequiredWDs + wsConflicts.Range("C" & i).Value
    Next i

'Display a message (debugging)
    MsgBox "You currently have " & RequiredCoverages & " required coverages and " & RequiredWDs & " required weekend duties."

'Count the number of coverage slips and weekend duty slips
Dim FirstCoverageSlipRow As Integer
    FirstCoverageSlipRow = 1
Dim LastCoverageSlipRow As Integer
    LastCoverageSlipRow = wsCoverageSlips.UsedRange.Rows.Count
Dim NumCoverageSlips As Integer
    NumCoverageSlips = (LastCoverageSlipRow - FirstCoverageSlipRow + 1)
Dim FirstWDSlipRow As Integer
    FirstWDSlipRow = 1
Dim LastWDSlipRow As Integer
    LastWDSlipRow = wsWDSlips.UsedRange.Rows.Count
Dim NumWDSlips As Integer
    NumWDSlips = (LastWDSlipRow - FirstWDSlipRow + 1)

'Check to make sure there are enough required shifts for slips
    If RequiredCoverages <> NumCoverageSlips Then
        MsgBox "The number of shifts you require (Columns B & C on Conflicts sheet) does not match the number of slips you've entered.  You have " & RequiredCoverages & " required coverages and " & NumCoverageSlips & " coverage slips.  You have " & RequiredWDs & " required weekend duties and " & NumWDSlips & " weekend duty slips.  Please correct this error and retry."
        Exit Sub
    Else
        'Debugging
        'MsgBox "The number of shifts you require (Columns B & C on Conflicts sheet) matches the number of slips you've entered.  You have " & RequiredCoverages & " required coverages and " & NumCoverageSlips & " coverage slips.  You have " & RequiredWDs & " required weekend duties and " & NumWDSlips & " weekend duty slips."
    End If

'Massive loop to assign coverages to random staff members
    Dim NumRemainingCoverages As Integer
        NumRemainingCoverages = NumCoverageSlips
    Dim SlipRowNumber As Integer
        SlipRowNumber = FirstCoverageSlipRow
'Loop for Column A
    Do Until (SlipRowNumber = LastCoverageSlipRow + 1)
        'Get a random staff member row
        StaffMemberRow = GetRandomStaffMemberRow(FirstStaffMemberRow, LastStaffMemberRow)

        'Check to make sure the staff member has remaining required coverages
        If wsConflicts.Range("B" & StaffMemberRow).Value > 0 Then

            'Check to make sure the staff member can sit the day of the week
                Dim CurrentDate As Date
                    CurrentDate = wsCoverageSlips.Range("A" & SlipRowNumber).Value
                Dim CurrentDay As Integer
                    CurrentDay = Weekday(CurrentDate)
                Dim CurrentDayColumn As String
                    If CurrentDay = 1 Then CurrentDayColumn = "D"
                    If CurrentDay = 2 Then CurrentDayColumn = "E"
                    If CurrentDay = 3 Then CurrentDayColumn = "F"
                    If CurrentDay = 4 Then CurrentDayColumn = "G"
                    If CurrentDay = 5 Then CurrentDayColumn = "H"
                    If CurrentDay = 6 Then CurrentDayColumn = "I"
                    If CurrentDay = 7 Then CurrentDayColumn = "J"
                If wsConflicts.Range(CurrentDayColumn & StaffMemberRow).Value = "" Then

                'Check to make sure the staff member does not have a date conflict
                    Dim ColumnNumber As Integer
                    Dim ColumnLetterText As String
                    Dim CoverageDateConflicts As Integer
                        CoverageDateConflicts = 0
                    For ColumnNumber = 11 To 20
                        ColumnLetterText = ColumnLetter(ColumnNumber)
                        Dim CoverageSlipDate As Date
                        If IsDate(wsConflicts.Range(ColumnLetterText & StaffMemberRow).Value) = True Then
                            CoverageSlipDate = wsConflicts.Range(ColumnLetterText & StaffMemberRow).Value
                        Else
                            CoverageSlipDate = DateValue("01/01/1900")
                        End If
                        If CurrentDate = CoverageSlipDate Then
                            CoverageDateConflicts = CoverageDateConflicts + 1
                        End If
                    Next ColumnNumber
                    If CoverageDateConflicts = 0 Then

                    'Assign the coverage
                        Dim BlankCoverageOutputRow As Integer
                            BlankCoverageOutputRow = wsCoverageOutput.UsedRange.Rows.Count + 1
                        wsCoverageOutput.Range("A" & BlankCoverageOutputRow).Value = wsConflicts.Range("A" & StaffMemberRow).Value
                        wsCoverageOutput.Range("B" & BlankCoverageOutputRow).Value = CurrentDate

                    'Reduce the staff member's required coverages by 1
                        Dim CurrentRequirements As Integer
                            CurrentRequirements = wsConflicts.Range("B" & StaffMemberRow).Value
                            wsConflicts.Range("B" & StaffMemberRow).Value = CurrentRequirements - 1

                    'Reduce the number of remaning coverages by 1
                        NumRemainingCoverages = NumRemainingCoverages - 1

                    'Increase the slip row number by 1
                        SlipRowNumber = SlipRowNumber + 1

                    'Message box for debugging
                        'MsgBox "Coverage Date (" & CurrentDate & ") assigned to " & wsConflicts.Range("A" & StaffMemberRow).Value & "."

                    End If 'End date check

                End If 'End day check

        End If 'End requirements check

    Loop 'End loop for column A

End Sub

'------------------------------------------------------------'
Public Function GetRandomStaffMemberRow(FirstStaffMemberRow As Integer, LastStaffMemberRow As Integer)
'Pick a random number between the first staff member row and the last
    Call Randomize
    GetRandomStaffMemberRow = Int((LastStaffMemberRow - FirstStaffMemberRow + 1) * Rnd + FirstStaffMemberRow)
End Function
Était-ce utile?

La solution 2

Alors je suis allé de l'avant et j'ai développé ma propre solution à ce problème - ce n'est pas parfait et ce n'est probablement pas le meilleur moyen de gérer le scénario. Mais cela fonctionne et cela résolva mon problème en quelques minutes au lieu d'heures d'apprentissage d'autres méthodes.

Fondamentalement, j'ai créé deux nouvelles variables "comptoir". Le premier est échoué. Chaque fois que la procédure essaie un membre du personnel aléatoire mais s'inscrit dans un conflit, il incrémente la battamate de 1. Chaque fois que le membre du personnel aléatoire est un match réussi (pas de conflits), il réinitialise la défaillance de 0. Si, à tout moment, échoué= 100, Il quitte immédiatement la boucle et commence. En d'autres termes, s'il essaie 100 membres du personnel aléatoire de suite sans trouver de match, je suppose que cela ne va pas trouver un match et simplement couper mes pertes.

La deuxième variable, les affectations, sont incrémentées de 1 chaque fois que la procédure effectue une mission réussie. Lorsque ce numéro est égal au nombre de décalages que la procédure est censée attribuer, il quitte immédiatement la boucle.

Pour ce faire, je devais utiliser quelques commandes interdites 'goto' (je n'étais pas sûre de la sortie de la boucle. Vous pouvez quitter une boucle avec sortie, mais je crois que cela n'est pas valide pour faire boucles. J'ai fini par avoir besoin de deux goto, un pour quitter la boucle et une pour revenir au début de la procédure. Je m'assurai aussi que les cellules de la feuille de calcul qui changent au cours de la procédure sont réinitialisées à leur état d'origine avant de pouvoir Réessaie la procédure d'affectation.

Je vais enregistrer tout le monde la peine de lire à travers la version étendue du code, mais dans la forme 'pseudo-code', on ressemble à ceci:

Retry: 'Label for GoTo command

Do Until (CoverageRowNumber = LastCoverageSlipRow + 1)
    Get a Random Staff Member by RNG
    If staff member still needs more shifts (see Requirements columns) Then
        If staff member does not have an "X" under the day of the week Then
            If staff member does not have a matching date conflict Then
                'Assign the coverage
                'Increase CoverageRowNumber
                Assignments = Assignments + 1
            Else
                FailedAttempts = FailedAttempts + 1                
            End If
        Else
            FailedAttempts = FailedAttempts + 1
        End If
    Else
        FailedAttempts = FailedAttempts + 1
    End If
    If FailedAttempts > 100 Then
        GoTo ExitLoop
    End If
Loop

ExitLoop: 'Label for GoTo command
    If Assignments <> NumCoverageSlips Then
        GoTo Retry
    End If        

'Do rest of procedure

Encore une fois, il peut y avoir (et est certainement) un moyen plus élégant et "correct" d'accomplir la tâche à accomplir. Cette méthode a fonctionné pour moi avec l'environnement donné. Grâce à ceux qui ont fourni des solutions - même si j'ai fini par passer une direction différente, ils ont fourni une grande nourriture à la pensée et m'ont aidé à apprendre un groupe de nouvelles méthodes (en particulier l'idée de classe de @stenci).

Merci tout.

Autres conseils

La question est trop ouverte pour une réponse détaillée, j'essaie donc avec quelques lignes directrices.J'espère que ça aide.

j'utiliserais un cours Solution avec les membres suivants :

Solution.ReadInputFromSheet() lit le tableau de la feuille aux élèves de la classe

Solution.GenerateRandom() crée une nouvelle solution aléatoire.Essayez de trouver un équilibre entre l'intelligence (ajoutez un peu de logique pour éviter les solutions totalement aléatoires) et la vitesse (ne restez pas bloqué, sortez après avoir essayé 10 ou 50 nombres aléatoires qui ne fonctionnent pas), mais la vitesse est plus importante

Solution.Quality() As Double calcule la qualité de la solution.Par exemple, une solution qui n'est pas valide renvoie 0, si Joe a 10 équipes consécutives renvoie 20, si les équipes sont mieux réparties renvoie 100.

Solution.WriteOnSheet() écrivez les données des membres de la classe sur la feuille.

Solution.Clone() As Solution() crée un nouveau Solution instance avec les mêmes données

Faites un cycle qui crée une solution, vérifie si sa qualité est meilleure que la solution de meilleure qualité trouvée jusqu'à présent, s'il vaut mieux la conserver, sinon allez calculer une autre solution.

Set BestS = New Solution
BestS.ReadInputFromSheet
BestS.GenerateRandom()

Set S = New Solution
S.ReadInputFromSheet
For I = 1 To 10000
  S.GenerateRandom()
  If S.Quality() > BestS.Quality() Then Set BestS = S.Clone()
Next I
BestS.WriteOnSheet

Au lieu de 10 000, vous pouvez utiliser Timer pour l'exécuter pendant un nombre fini de secondes, ou faire un bouton pour l'interrompre lorsque vous revenez de la pause déjeuner.

Il vaut mieux disposer d’une fonction de générateur de solutions plus rapide que de risquer de se retrouver coincé avec une solution difficile (voire impossible).

Pour une fonction de générateur de solutions plus intelligente, j'ai besoin de plus de détails sur les règles.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top